fix: 常用方、患者管理

This commit is contained in:
2025-12-26 09:19:22 +08:00
parent f4e51dc773
commit d4b9cc32d5
16 changed files with 6172 additions and 40 deletions

View File

@@ -0,0 +1,498 @@
<script lang="ts" setup>
/**
* 药品搜索选择组件(自定义实现)
*
* @description 自定义药品搜索下拉选择器不依赖antd的Select组件
* - 支持中药/西药类型切换
* - 下拉选项展示:药品图片、名称、供应商、规格、价格
* - 选择时返回完整药品数据(包含默认用法字段)
* @author 系统
* @date 2024
*/
import { ref, watch, onMounted, onUnmounted } from 'vue';
import { SearchOutlined, LoadingOutlined } from '@ant-design/icons-vue';
import { Input, Spin } from 'ant-design-vue';
import { debounce } from 'lodash-es';
import { getProductListDoctorReception } from '#/views/doctor/doctor-reception/api';
// ==================== Props 定义 ====================
interface Props {
/**
* 药品类型
* 1-中药2-西药
*/
type?: number;
/**
* 占位提示文本
*/
placeholder?: string;
/**
* 诊所ID
*/
storeId?: number;
/**
* 是否禁用
*/
disabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
type: 1,
placeholder: '输入药品名称搜索',
storeId: 2,
disabled: false,
});
// ==================== Emits 定义 ====================
const emit = defineEmits<{
/**
* 选中药品时触发
* @param drug 完整的药品数据
*/
(e: 'select', drug: any): void;
}>();
// ==================== 响应式数据 ====================
/**
* 搜索关键词
*/
const searchKeyword = ref('');
/**
* 是否正在搜索
*/
const isSearching = ref(false);
/**
* 搜索结果列表
*/
const searchResults = ref<any[]>([]);
/**
* 是否显示下拉列表
*/
const showDropdown = ref(false);
/**
* 组件容器引用
*/
const containerRef = ref<HTMLElement | null>(null);
/**
* 当前高亮的选项索引(用于键盘导航)
*/
const highlightIndex = ref(-1);
// ==================== 方法定义 ====================
/**
* 搜索药品
* @param keyword 搜索关键词
* @description 调用后端API搜索药品支持按名称和拼音搜索
*/
const searchDrugs = debounce(async (keyword: string) => {
if (!keyword || keyword.length < 1) {
searchResults.value = [];
showDropdown.value = false;
return;
}
isSearching.value = true;
showDropdown.value = true;
try {
const res = await getProductListDoctorReception({
name: keyword,
type: props.type,
store_id: props.storeId,
});
// 处理返回数据
if (Array.isArray(res)) {
searchResults.value = res.map((item: any) => ({
// 保留原始数据
...item,
// 提取常用字段便于访问
_id: item.id,
_drugId: item.drug_id || item.drug?.id,
_drugName: item.drug?.drug_name || item.drug_name || '',
_image: item.drug?.image || '',
_specification: item.drug?.specification || '',
_supplier: item.drug?.supplier?.name || '',
_price: item.price || 0,
// 默认用法字段
_timeId: item.drug?.time_id || 0,
_typeId: item.drug?.type_id || 0,
_frequencyId: item.drug?.frequency_id || 0,
_unitId: item.drug?.unit_id || 0,
_number: item.drug?.number || 1,
// 用法名称
_useNum: item.drug?.useNum,
_useType: item.drug?.useType,
_useFrequency: item.drug?.useFrequency,
}));
} else {
searchResults.value = [];
}
} catch (error) {
console.error('搜索药品失败:', error);
searchResults.value = [];
} finally {
isSearching.value = false;
}
}, 300);
/**
* 处理输入变化
* @param e 输入事件
*/
function handleInput(e: Event) {
const target = e.target as HTMLInputElement;
searchKeyword.value = target.value;
highlightIndex.value = -1;
searchDrugs(target.value);
}
/**
* 处理输入框获得焦点
*/
function handleFocus() {
if (searchResults.value.length > 0) {
showDropdown.value = true;
}
}
/**
* 处理选中药品
* @param drug 选中的药品数据
*/
function handleSelectDrug(drug: any) {
emit('select', drug);
// 清空搜索状态
searchKeyword.value = '';
searchResults.value = [];
showDropdown.value = false;
highlightIndex.value = -1;
}
/**
* 处理键盘事件
* @param e 键盘事件
*/
function handleKeydown(e: KeyboardEvent) {
if (!showDropdown.value || searchResults.value.length === 0) return;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
highlightIndex.value = Math.min(
highlightIndex.value + 1,
searchResults.value.length - 1,
);
break;
case 'ArrowUp':
e.preventDefault();
highlightIndex.value = Math.max(highlightIndex.value - 1, 0);
break;
case 'Enter':
e.preventDefault();
if (highlightIndex.value >= 0) {
handleSelectDrug(searchResults.value[highlightIndex.value]);
}
break;
case 'Escape':
showDropdown.value = false;
highlightIndex.value = -1;
break;
}
}
/**
* 处理点击外部关闭下拉
* @param e 点击事件
*/
function handleClickOutside(e: MouseEvent) {
if (containerRef.value && !containerRef.value.contains(e.target as Node)) {
showDropdown.value = false;
highlightIndex.value = -1;
}
}
// ==================== 生命周期 ====================
onMounted(() => {
document.addEventListener('click', handleClickOutside);
});
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside);
});
// ==================== 监听 ====================
/**
* 监听类型变化,清空搜索结果
*/
watch(
() => props.type,
() => {
searchResults.value = [];
showDropdown.value = false;
searchKeyword.value = '';
},
);
</script>
<template>
<div ref="containerRef" class="drug-search-select">
<!-- 搜索输入框 -->
<Input
v-model:value="searchKeyword"
:placeholder="placeholder"
:disabled="disabled"
allow-clear
@input="handleInput"
@focus="handleFocus"
@keydown="handleKeydown"
>
<template #prefix>
<LoadingOutlined v-if="isSearching" class="text-gray-400" />
<SearchOutlined v-else class="text-gray-400" />
</template>
</Input>
<!-- 下拉列表 -->
<div v-if="showDropdown" class="drug-dropdown">
<!-- 加载中 -->
<div v-if="isSearching" class="drug-dropdown__loading">
<Spin size="small" />
<span>搜索中...</span>
</div>
<!-- 无结果 -->
<div
v-else-if="searchResults.length === 0"
class="drug-dropdown__empty"
>
暂无匹配药品
</div>
<!-- 结果列表 -->
<div v-else class="drug-dropdown__list">
<div
v-for="(item, index) in searchResults"
:key="item._id"
class="drug-item"
:class="{ 'drug-item--active': index === highlightIndex }"
@click="handleSelectDrug(item)"
@mouseenter="highlightIndex = index"
>
<!-- 药品图片 -->
<div class="drug-item__image">
<img
v-if="item._image"
:src="item._image"
alt=""
class="drug-item__img"
@error="(e) => (e.target as HTMLImageElement).style.display = 'none'"
/>
<div v-else class="drug-item__img-placeholder">无图</div>
</div>
<!-- 药品信息 -->
<div class="drug-item__info">
<!-- 药品名称 -->
<div class="drug-item__name">{{ item._drugName }}</div>
<!-- 规格和供应商 -->
<div class="drug-item__meta">
<span v-if="item._specification" class="drug-item__spec">
规格{{ item._specification }}
</span>
<span v-if="item._supplier" class="drug-item__supplier">
供应商{{ item._supplier }}
</span>
</div>
</div>
<!-- 价格 -->
<div class="drug-item__price">
¥{{ Number(item._price).toFixed(2) }}
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.drug-search-select {
position: relative;
width: 100%;
}
.drug-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1050;
margin-top: 4px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
max-height: 400px;
overflow: hidden;
&__loading,
&__empty {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 24px;
color: #999;
font-size: 14px;
}
&__list {
max-height: 400px;
overflow-y: auto;
padding: 4px 0;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #d9d9d9;
border-radius: 3px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
}
.drug-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
cursor: pointer;
transition: background-color 0.2s;
&:hover,
&--active {
background-color: #f5f7fa;
}
&__image {
flex-shrink: 0;
width: 48px;
height: 48px;
}
&__img {
width: 48px;
height: 48px;
object-fit: cover;
border-radius: 6px;
border: 1px solid #f0f0f0;
}
&__img-placeholder {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
border-radius: 6px;
font-size: 12px;
color: #bbb;
}
&__info {
flex: 1;
min-width: 0;
overflow: hidden;
}
&__name {
font-size: 14px;
font-weight: 500;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 4px;
}
&__meta {
display: flex;
flex-wrap: wrap;
gap: 12px;
font-size: 12px;
color: #999;
}
&__spec {
color: #666;
}
&__supplier {
color: #1890ff;
}
&__price {
flex-shrink: 0;
font-size: 16px;
font-weight: 600;
color: #ff4d4f;
}
}
/* 暗色模式适配 */
.dark {
.drug-dropdown {
background: #1f2937;
border-color: #374151;
}
.drug-item {
&:hover,
&--active {
background-color: #374151;
}
&__img-placeholder {
background: #374151;
color: #6b7280;
}
&__name {
color: #f3f4f6;
}
&__meta {
color: #9ca3af;
}
&__spec {
color: #d1d5db;
}
&__supplier {
color: #60a5fa;
}
}
}
</style>

View File

@@ -0,0 +1,9 @@
/**
* 药品搜索选择组件
*
* @description 封装药品搜索下拉选择器,展示药品详细信息
* @author 系统
* @date 2024
*/
export { default as DrugSearchSelect } from './drug-search-select.vue';

View File

@@ -85,9 +85,11 @@ export const usePrescriptionStore = defineStore('prescription', () => {
const selectedStoreId = ref<number | null>(null); // 用户选择的诊所ID const selectedStoreId = ref<number | null>(null); // 用户选择的诊所ID
const sendMode = ref(0); // 0=默认使用医生当前诊所1=自定义(使用挂号诊所) const sendMode = ref(0); // 0=默认使用医生当前诊所1=自定义(使用挂号诊所)
// 获取localStorage key修改为在线复诊前缀 // 获取localStorage key按类型分开存储
const getStorageKey = () => const getStorageKey = (category?: number) => {
`onlineConsultation-prescriptionData${currentRegisterId.value}`; const cat = category ?? activeCategory.value;
return `onlineConsultation-prescriptionData_${cat}_${currentRegisterId.value}`;
};
// 计算属性 // 计算属性
const totalProductCost = computed(() => { const totalProductCost = computed(() => {
@@ -201,6 +203,27 @@ export const usePrescriptionStore = defineStore('prescription', () => {
// 设置当前注册ID // 设置当前注册ID
currentRegisterId.value = registerId; currentRegisterId.value = registerId;
// 恢复之前保存的 activeCategory
const savedCategory = localStorage.getItem(
`onlineConsultation-activeCategory${registerId}`
);
if (savedCategory) {
activeCategory.value = Number.parseInt(savedCategory);
} else {
// 检查哪个存储键有数据
const chineseData = localStorage.getItem(
`onlineConsultation-prescriptionData_1_${registerId}`
);
const westData = localStorage.getItem(
`onlineConsultation-prescriptionData_2_${registerId}`
);
if (chineseData && JSON.parse(chineseData).length > 0) {
activeCategory.value = 1;
} else if (westData && JSON.parse(westData).length > 0) {
activeCategory.value = 2;
}
}
// 加载localStorage数据 // 加载localStorage数据
loadFromLocalStorage(); loadFromLocalStorage();
@@ -228,6 +251,27 @@ export const usePrescriptionStore = defineStore('prescription', () => {
// 设置当前注册ID // 设置当前注册ID
currentRegisterId.value = registerId; currentRegisterId.value = registerId;
// 恢复之前保存的 activeCategory
const savedCategory = localStorage.getItem(
`onlineConsultation-activeCategory${registerId}`
);
if (savedCategory) {
activeCategory.value = Number.parseInt(savedCategory);
} else {
// 检查哪个存储键有数据
const chineseData = localStorage.getItem(
`onlineConsultation-prescriptionData_1_${registerId}`
);
const westData = localStorage.getItem(
`onlineConsultation-prescriptionData_2_${registerId}`
);
if (chineseData && JSON.parse(chineseData).length > 0) {
activeCategory.value = 1;
} else if (westData && JSON.parse(westData).length > 0) {
activeCategory.value = 2;
}
}
// 加载localStorage数据 // 加载localStorage数据
loadFromLocalStorage(); loadFromLocalStorage();
@@ -458,6 +502,33 @@ export const usePrescriptionStore = defineStore('prescription', () => {
} }
}; };
// 切换药品用法编辑状态
const toggleEditDrug = (index: number) => {
const newDrugs = [...currentDrugs.value];
newDrugs[index].isEditing = !newDrugs[index].isEditing;
updateCurrentDrugs(newDrugs);
};
// 保存药品用法编辑
const saveEditDrug = (index: number) => {
const newDrugs = [...currentDrugs.value];
const drug = newDrugs[index];
// 更新 ID 字段
drug.type_id = drug.use_type?.id;
drug.frequency_id = drug.use_frequency?.id;
drug.time_id = drug.use_num?.id;
drug.unit_id = drug.unit?.id;
drug.isEditing = false;
updateCurrentDrugs(newDrugs);
};
// 更新药品用法字段
const updateDrugUsage = (index: number, field: string, value: any) => {
const newDrugs = [...currentDrugs.value];
newDrugs[index][field] = value;
updateCurrentDrugs(newDrugs);
};
// 新药品操作 // 新药品操作
const selectNewDrugInfo = () => { const selectNewDrugInfo = () => {
const check = currentDrugs.value.find((v) => v.id === newDrugInfo.value.id); const check = currentDrugs.value.find((v) => v.id === newDrugInfo.value.id);
@@ -791,12 +862,26 @@ export const usePrescriptionStore = defineStore('prescription', () => {
}; };
const changeCategory = (categoryValue: number) => { const changeCategory = (categoryValue: number) => {
activeCategory.value = categoryValue; // 先保存当前类型的药品数据
updateCurrentDrugs([]); localStorage.setItem(
getStorageKey(activeCategory.value),
JSON.stringify(currentDrugs.value),
);
// 保存 Tab 类型
localStorage.setItem( localStorage.setItem(
`onlineConsultation-activeCategory${currentRegisterId.value}`, `onlineConsultation-activeCategory${currentRegisterId.value}`,
categoryValue.toString(), categoryValue.toString(),
); );
activeCategory.value = categoryValue;
// 加载新类型对应的药品数据
const stored = localStorage.getItem(getStorageKey(categoryValue));
if (stored) {
currentDrugs.value.splice(0, currentDrugs.value.length, ...JSON.parse(stored));
} else {
currentDrugs.value.splice(0, currentDrugs.value.length);
}
}; };
// 工具函数 // 工具函数
@@ -875,6 +960,9 @@ export const usePrescriptionStore = defineStore('prescription', () => {
updateDrugQuantity, updateDrugQuantity,
increment, increment,
decrement, decrement,
toggleEditDrug,
saveEditDrug,
updateDrugUsage,
selectNewDrugInfo, selectNewDrugInfo,
selectOldDrugInfo, selectOldDrugInfo,
setSelectChineseIndex, setSelectChineseIndex,

View File

@@ -1,6 +1,8 @@
import { requestClient } from '#/api/request'; import { requestClient } from '#/api/request';
const prefix = 'chat-friends/'; const prefix = 'chat-friends/';
const commonPrescriptionPrefix = 'common-prescription/';
/** /**
* 获取当前登录的诊所信息 * 获取当前登录的诊所信息
* @param data * @param data
@@ -26,3 +28,143 @@ export async function getChatMessageRegisterInfoApi(data: any = {}) {
// return requestClient.post<any>(`${prefix}messages-by-room-id`, { params: data }); // return requestClient.post<any>(`${prefix}messages-by-room-id`, { params: data });
return requestClient.get<any>(`${prefix}chat-register-info`, { params: data }); return requestClient.get<any>(`${prefix}chat-register-info`, { params: data });
} }
// ==================== 常用方管理 API ====================
/**
* 常用方药品数据结构(西药/中成药)
* @description 西药药品的详细信息
*/
export interface CommonPrescriptionWestDrug {
/** 药品ID */
drug_id?: number;
/** 药品ID兼容字段 */
id?: number;
/** 药品名称 */
drug_name: string;
/** 每次用量 */
number?: number;
/** 购买数量 */
select_number?: number;
/** 单价 */
price?: number;
/** 使用时间ID */
time_id?: number;
/** 使用类型ID */
type_id?: number;
/** 使用频率ID */
frequency_id?: number;
/** 单位ID */
unit_id?: number;
/** 药品图片 */
image?: string;
/** 说明书 */
instruction?: string;
/** 使用时间信息 */
use_num?: { name?: string };
/** 使用类型信息 */
use_type?: { name?: string };
/** 使用频率信息 */
use_frequency?: { name?: string };
/** 单位信息 */
unit?: { name?: string };
}
/**
* 常用方药品数据结构(中药/颗粒药)
* @description 中药/颗粒药药品的详细信息
*/
export interface CommonPrescriptionChineseDrug {
/** 药品ID */
drug_id?: number;
/** 药品ID兼容字段 */
id?: number;
/** 药品名称 */
drug_name: string;
/** 克数/用量 */
number?: number;
/** 单价 */
price?: number;
/** 用法ID */
way_id?: number;
}
/**
* 保存西药常用方
* @description 将当前处方保存为西药常用方模板,后端会自动创建 recipe 记录
* @param data 常用方数据
*/
export async function saveWestCommonPrescriptionApi(data: {
/** 常用方名称 */
name: string;
/** 药品详细信息数组 */
drugs: CommonPrescriptionWestDrug[];
/** 诊所ID */
store_id: number;
/** 临床诊断(可选) */
clinical_diagnose?: string;
/** 医嘱(可选) */
doctor_order?: string;
/** 类别 1-自费 2-医保(可选) */
category?: string;
}) {
return requestClient.post<any>(`${commonPrescriptionPrefix}save-west`, data);
}
/**
* 保存中药常用方
* @description 将当前处方保存为中药常用方模板,后端会自动创建 recipe 记录
* @param data 常用方数据
*/
export async function saveChineseCommonPrescriptionApi(data: {
/** 常用方名称 */
name: string;
/** 药品详细信息数组 */
drugs: CommonPrescriptionChineseDrug[];
/** 诊所ID */
store_id: number;
/** 临床诊断(可选) */
clinical_diagnose?: string;
/** 医嘱(可选) */
doctor_order?: string;
/** 类别 1-自费 2-医保(可选) */
category?: string;
/** 剂量/天数可选默认7 */
dosage?: number;
/** 每日次数可选默认2 */
day_dosage?: number;
}) {
return requestClient.post<any>(
`${commonPrescriptionPrefix}save-chinese`,
data,
);
}
/**
* 保存颗粒药常用方
* @description 将当前处方保存为颗粒药常用方模板,后端会自动创建 recipe 记录
* @param data 常用方数据
*/
export async function saveGranularCommonPrescriptionApi(data: {
/** 常用方名称 */
name: string;
/** 药品详细信息数组 */
drugs: CommonPrescriptionChineseDrug[];
/** 诊所ID */
store_id: number;
/** 临床诊断(可选) */
clinical_diagnose?: string;
/** 医嘱(可选) */
doctor_order?: string;
/** 类别 1-自费 2-医保(可选) */
category?: string;
/** 剂量/天数可选默认7 */
dosage?: number;
/** 每日次数可选默认2 */
day_dosage?: number;
}) {
return requestClient.post<any>(
`${commonPrescriptionPrefix}save-granular`,
data,
);
}

View File

@@ -8,6 +8,7 @@ import {
DownOutlined, DownOutlined,
MinusOutlined, MinusOutlined,
PlusOutlined, PlusOutlined,
SaveOutlined,
UpOutlined, UpOutlined,
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import { import {
@@ -18,6 +19,7 @@ import {
Descriptions, Descriptions,
Image, Image,
ImagePreviewGroup, ImagePreviewGroup,
Input,
InputNumber, InputNumber,
message, message,
RadioButton, RadioButton,
@@ -31,6 +33,10 @@ import {
TimelineItem, TimelineItem,
} from 'ant-design-vue'; } from 'ant-design-vue';
import {
saveChineseCommonPrescriptionApi,
saveWestCommonPrescriptionApi,
} from '#/views/business/chat/api';
import { usePrescriptionStore } from '#/store/prescription'; import { usePrescriptionStore } from '#/store/prescription';
// import { getTraditionalChineseMedicineAllApi } from '#/views/doctor/doctor-reception/api'; // import { getTraditionalChineseMedicineAllApi } from '#/views/doctor/doctor-reception/api';
// 导入子组件 // 导入子组件
@@ -38,6 +44,8 @@ import DiagnosisModal from '#/views/doctor/doctor-reception/components/Diagnosis
import DoctorOrderModal from '#/views/doctor/doctor-reception/components/DoctorOrderModal.vue'; import DoctorOrderModal from '#/views/doctor/doctor-reception/components/DoctorOrderModal.vue';
import PrescriptionDetail from '#/views/doctor/doctor-reception/components/PrescriptionDetail.vue'; import PrescriptionDetail from '#/views/doctor/doctor-reception/components/PrescriptionDetail.vue';
import WesternModal from '#/views/doctor/doctor-reception/components/WesternModal.vue'; import WesternModal from '#/views/doctor/doctor-reception/components/WesternModal.vue';
// 常用方选择弹窗组件
import CommonPrescriptionModal from '#/views/doctor/doctor-reception/components/CommonPrescriptionModal.vue';
import StoreConfirmModal from './StoreConfirmModal.vue'; import StoreConfirmModal from './StoreConfirmModal.vue';
const splitString = (input: string) => input.split(','); const splitString = (input: string) => input.split(',');
@@ -49,6 +57,14 @@ const previewImage = ref([]);
const showNewDrugModal = ref(false); const showNewDrugModal = ref(false);
const doctorSecondSignModal = ref(false); const doctorSecondSignModal = ref(false);
// ==================== 保存常用方相关状态 ====================
/** 保存常用方弹窗是否显示 */
const showSaveCommonPrescriptionModal = ref(false);
/** 常用方名称输入 */
const commonPrescriptionName = ref('');
/** 是否正在保存常用方 */
const isSavingCommonPrescription = ref(false);
const categories = [ const categories = [
{ label: '中药', value: 1 }, { label: '中药', value: 1 },
{ label: '中成(西)药', value: 2 }, { label: '中成(西)药', value: 2 },
@@ -124,6 +140,11 @@ const [StoreConfirmModalComponent, storeConfirmModalApi] = useVbenModal({
connectedComponent: StoreConfirmModal, connectedComponent: StoreConfirmModal,
}); });
// ==================== 常用方选择弹窗 ====================
const [CommonPrescriptionModals, CommonPrescriptionModalApi] = useVbenModal({
connectedComponent: CommonPrescriptionModal,
});
const setVisible = (value: boolean, instruction = ''): void => { const setVisible = (value: boolean, instruction = ''): void => {
previewImage.value = []; previewImage.value = [];
if (instruction === '') { if (instruction === '') {
@@ -293,6 +314,204 @@ const filterOption = (input: string, option: any) => {
option.code.includes(input) option.code.includes(input)
); );
}; };
// ==================== 选择常用方相关方法 ====================
/**
* 打开常用方选择弹窗
* @description 打开常用方弹窗,选择后自动填充药品到处方中
*/
function openCommonPrescriptionModal() {
CommonPrescriptionModalApi.setData({
type: prescriptionStore.activeCategory,
onSelect: handleSelectCommonPrescription,
});
CommonPrescriptionModalApi.open();
}
/**
* 处理选择常用方
* @param data 常用方数据包含prescription和recipes
* @param type 类型1-中药2-西药3-颗粒药
* @description 选择常用方后,将药品列表填充到当前处方中
*/
function handleSelectCommonPrescription(data: any, type: number) {
const { prescription, recipes } = data;
// 根据类型处理不同的药品数据
if (type === 2) {
// 西药(中成药)处方
recipes.forEach((recipe: any) => {
const newProduct = {
index_id: recipe.drug_id || recipe.id,
id: recipe.drug_id || recipe.id,
drug_name: recipe.drug_name,
number: recipe.number || 1,
use_num: recipe.use_time,
use_type: recipe.use_type,
use_frequency: recipe.use_frequency,
unit: recipe.west_unit,
price: recipe.price || 0,
time_id: recipe.time_id,
type_id: recipe.type_id,
frequency_id: recipe.frequency_id,
unit_id: recipe.unit_id,
image: recipe.image,
instruction: recipe.instruction,
type: recipe.type,
select_number: recipe.select_number || 1,
};
// 检查是否已存在
const existItem = prescriptionStore.currentDrugs.find(
(item: any) => item.id === newProduct.id,
);
if (!existItem) {
prescriptionStore.currentDrugs.push(newProduct);
}
});
} else {
// 中药/颗粒药处方
recipes.forEach((recipe: any) => {
const newProduct = {
index_id: recipe.drug_id || recipe.id,
id: recipe.drug_id || recipe.id,
drug_name: recipe.drug_name || recipe.name,
number: recipe.number || 1,
price: recipe.price || 0,
way_id: recipe.way_id || 0,
select_number: 1,
};
// 检查是否已存在
const existItem = prescriptionStore.currentDrugs.find(
(item: any) => item.id === newProduct.id,
);
if (!existItem) {
prescriptionStore.currentDrugs.push(newProduct);
}
});
}
// 更新诊断和医嘱
if (prescription.clinical_diagnose) {
prescriptionStore.diagnosis = prescription.clinical_diagnose;
}
if (prescription.doctor_order) {
prescriptionStore.medicalAdvice = prescription.doctor_order;
}
// 保存到本地存储
prescriptionStore.saveToLocalStorage();
message.success('已选择常用方,药品已填充到处方中');
}
// ==================== 保存常用方相关方法 ====================
/**
* 打开保存常用方弹窗
* @description 检查是否有药品可保存,然后打开弹窗
*/
const openSaveCommonPrescriptionModal = () => {
// 检查是否有药品
if (prescriptionStore.currentDrugs.length === 0) {
message.warning('请先添加药品后再保存为常用方');
return;
}
// 清空输入框
commonPrescriptionName.value = '';
// 打开弹窗
showSaveCommonPrescriptionModal.value = true;
};
/**
* 保存当前处方为常用方
* @description 根据当前药品类型(中药/西药)调用不同的保存接口
* 后端会自动根据药品详细信息创建 recipe 记录
*/
const saveAsCommonPrescription = async () => {
// 验证名称
if (!commonPrescriptionName.value.trim()) {
message.warning('请输入常用方名称');
return;
}
// 验证是否有药品
if (prescriptionStore.currentDrugs.length === 0) {
message.warning('没有可保存的药品');
return;
}
isSavingCommonPrescription.value = true;
try {
// 根据处方类型选择不同的保存接口
if (prescriptionStore.activeCategory === 1) {
// 中药处方 - 构建药品详细信息数组
const drugs = prescriptionStore.currentDrugs.map((drug) => ({
drug_id: drug.id,
drug_name: drug.drug_name,
number: drug.number || 1,
price: drug.price || 0,
way_id: drug.way_id || 0,
}));
await saveChineseCommonPrescriptionApi({
name: commonPrescriptionName.value.trim(),
drugs,
store_id: prescriptionStore.myStoreId,
clinical_diagnose: prescriptionStore.diagnosis || '',
doctor_order: prescriptionStore.medicalAdvice || '',
category: String(prescriptionStore.category),
dosage: prescriptionStore.dosage || 7,
day_dosage: prescriptionStore.dayDosage || 2,
});
} else {
// 西药(中成药)处方 - 构建药品详细信息数组
const drugs = prescriptionStore.currentDrugs.map((drug) => ({
drug_id: drug.id,
drug_name: drug.drug_name,
number: drug.number || 1,
select_number: drug.select_number || 1,
price: drug.price || 0,
time_id: drug.time_id || 0,
type_id: drug.type_id || 0,
frequency_id: drug.frequency_id || 0,
unit_id: drug.unit_id || 0,
image: drug.image || '',
instruction: drug.instruction || '',
use_num: drug.use_num,
use_type: drug.use_type,
use_frequency: drug.use_frequency,
unit: drug.unit,
}));
await saveWestCommonPrescriptionApi({
name: commonPrescriptionName.value.trim(),
drugs,
store_id: prescriptionStore.myStoreId,
clinical_diagnose: prescriptionStore.diagnosis || '',
doctor_order: prescriptionStore.medicalAdvice || '',
category: String(prescriptionStore.category),
});
}
message.success('常用方保存成功');
showSaveCommonPrescriptionModal.value = false;
commonPrescriptionName.value = '';
} catch (error) {
console.error('保存常用方失败:', error);
message.error('保存常用方失败,请稍后重试');
} finally {
isSavingCommonPrescription.value = false;
}
};
/**
* 取消保存常用方
*/
const cancelSaveCommonPrescription = () => {
showSaveCommonPrescriptionModal.value = false;
commonPrescriptionName.value = '';
};
</script> </script>
<template> <template>
@@ -597,14 +816,31 @@ const filterOption = (input: string, option: any) => {
<RadioButton :value="2">医保</RadioButton> <RadioButton :value="2">医保</RadioButton>
</RadioGroup> </RadioGroup>
<Button <div class="flex gap-2">
v-if="prescriptionStore.activeCategory !== 1" <!-- 选择常用方按钮 -->
type="primary" <Button
@click="openWesternModal" type="default"
> @click="openCommonPrescriptionModal"
<PlusOutlined /> >
添加商品 选择常用方
</Button> </Button>
<!-- 保存为常用方按钮 -->
<Button
type="default"
@click="openSaveCommonPrescriptionModal"
>
<SaveOutlined />
保存为常用方
</Button>
<Button
v-if="prescriptionStore.activeCategory !== 1"
type="primary"
@click="openWesternModal"
>
<PlusOutlined />
添加商品
</Button>
</div>
</div> </div>
<!-- 中药药品列表 --> <!-- 中药药品列表 -->
@@ -955,11 +1191,63 @@ const filterOption = (input: string, option: any) => {
<p> <p>
<span>药品名称:{{ drug.drug_name }}</span> <span>药品名称:{{ drug.drug_name }}</span>
</p> </p>
<p> <!-- 西药用法显示/编辑模式 -->
<p v-if="prescriptionStore.activeCategory !== 1 && !drug.isEditing">
<span>用法:{{ <span>用法:{{
`${drug.use_type?.name}${drug.use_frequency?.name}${drug.use_num?.name},每次${drug.number}${drug.unit?.name}` `${drug.use_type?.name}${drug.use_frequency?.name}${drug.use_num?.name},每次${drug.number}${drug.unit?.name}`
}}</span> }}</span>
</p> </p>
<div v-else-if="prescriptionStore.activeCategory !== 1 && drug.isEditing" class="edit-usage-form">
<span>用法:</span>
<Select
:value="drug.use_type?.id"
size="small"
style="width: 80px"
@change="(val) => prescriptionStore.updateDrugUsage(index, 'use_type', prescriptionStore.drugUseType.find(item => item.id === val))"
>
<SelectOption v-for="item in prescriptionStore.drugUseType" :key="item.id" :value="item.id">
{{ item.name }}
</SelectOption>
</Select>
<Select
:value="drug.use_frequency?.id"
size="small"
style="width: 100px; margin-left: 4px"
@change="(val) => prescriptionStore.updateDrugUsage(index, 'use_frequency', prescriptionStore.drugUseFrequency.find(item => item.id === val))"
>
<SelectOption v-for="item in prescriptionStore.drugUseFrequency" :key="item.id" :value="item.id">
{{ item.name }}
</SelectOption>
</Select>
<Select
:value="drug.use_num?.id"
size="small"
style="width: 80px; margin-left: 4px"
@change="(val) => prescriptionStore.updateDrugUsage(index, 'use_num', prescriptionStore.drugTime.find(item => item.id === val))"
>
<SelectOption v-for="item in prescriptionStore.drugTime" :key="item.id" :value="item.id">
{{ item.name }}
</SelectOption>
</Select>
<span style="margin-left: 4px">每次</span>
<InputNumber
:value="drug.number"
:min="1"
size="small"
style="width: 60px; margin-left: 4px"
@change="(val) => prescriptionStore.updateDrugUsage(index, 'number', val)"
/>
<Select
:value="drug.unit?.id"
size="small"
style="width: 60px; margin-left: 4px"
@change="(val) => prescriptionStore.updateDrugUsage(index, 'unit', prescriptionStore.drugUnit.find(item => item.id === val))"
>
<SelectOption v-for="item in prescriptionStore.drugUnit" :key="item.id" :value="item.id">
{{ item.name }}
</SelectOption>
</Select>
</div>
</div> </div>
<div> <div>
<div class="quantity-control"> <div class="quantity-control">
@@ -980,6 +1268,21 @@ const filterOption = (input: string, option: any) => {
</div> </div>
<span>{{ drug.price }}</span> <span>{{ drug.price }}</span>
<div> <div>
<!-- 西药编辑/保存按钮 -->
<Button
v-if="prescriptionStore.activeCategory !== 1 && !drug.isEditing"
type="link"
@click="prescriptionStore.toggleEditDrug(index)"
>
编辑
</Button>
<Button
v-if="prescriptionStore.activeCategory !== 1 && drug.isEditing"
type="link"
@click="prescriptionStore.saveEditDrug(index)"
>
保存
</Button>
<Button <Button
type="link" type="link"
@click="prescriptionStore.removeDrug(index)" @click="prescriptionStore.removeDrug(index)"
@@ -1280,6 +1583,33 @@ const filterOption = (input: string, option: any) => {
要把【{{ prescriptionStore.newDrugInfo.name }}】添加到清单吗? 要把【{{ prescriptionStore.newDrugInfo.name }}】添加到清单吗?
</AntModal> </AntModal>
<!-- 保存常用方确认模态框 -->
<AntModal
v-model:open="showSaveCommonPrescriptionModal"
title="保存为常用方"
:confirm-loading="isSavingCommonPrescription"
@ok="saveAsCommonPrescription"
@cancel="cancelSaveCommonPrescription"
>
<div class="py-4">
<p class="mb-4 text-gray-600">
将当前处方({{ prescriptionStore.currentDrugs.length }}种药品)保存为常用方模板
</p>
<div class="flex items-center">
<span class="mr-2 whitespace-nowrap">常用方名称:</span>
<Input
v-model:value="commonPrescriptionName"
placeholder="请输入常用方名称"
:maxlength="50"
allow-clear
/>
</div>
<p class="mt-2 text-xs text-gray-400">
类型:{{ prescriptionStore.activeCategory === 1 ? '中药' : '西药(中成药)' }}
</p>
</div>
</AntModal>
<!-- 二次签名确认模态框 --> <!-- 二次签名确认模态框 -->
<AntModal <AntModal
v-model:open="doctorSecondSignModal" v-model:open="doctorSecondSignModal"
@@ -1319,6 +1649,8 @@ const filterOption = (input: string, option: any) => {
<DoctorOrderModals /> <DoctorOrderModals />
<PrescriptionDetailModal /> <PrescriptionDetailModal />
<StoreConfirmModalComponent /> <StoreConfirmModalComponent />
<!-- 常用方选择弹窗 -->
<CommonPrescriptionModals />
</Modal> </Modal>
</template> </template>

View File

@@ -143,3 +143,188 @@ export async function getTraditionalChineseMedicineAllApi() {
`${prefix}get-traditional-chinese-medicine-all`, `${prefix}get-traditional-chinese-medicine-all`,
); );
} }
// ==================== 常用方管理 API ====================
const commonPrescriptionPrefix = 'common-prescription/';
/**
* 常用方列表响应数据结构
* @description 包含西药、中药、颗粒药三种类型的常用方
*/
export interface CommonPrescriptionData {
/** 西药常用方列表 */
west_prescription: any[];
/** 西药详细药品信息 */
west: any[][];
/** 中药常用方列表 */
chin_prescription: any[];
/** 中药详细药品信息 */
chinese: any[][];
/** 颗粒药常用方列表 */
granular_prescription: any[];
/** 颗粒药详细药品信息 */
granular: any[][];
}
/**
* 获取常用方列表
* @description 获取当前医生的所有常用方(西药/中药/颗粒药)
* @param storeId 诊所ID可选
*/
export async function getCommonPrescriptionListApi(storeId?: number) {
return requestClient.get<CommonPrescriptionData>(
`${commonPrescriptionPrefix}list`,
{ params: { store_id: storeId } },
);
}
/**
* 获取常用方详情
* @description 根据ID和类型获取常用方的详细信息
* @param id 常用方ID
* @param type 类型west-西药chinese-中药granular-颗粒药
*/
export async function getCommonPrescriptionDetailApi(
id: number,
type: string,
) {
return requestClient.get<any[]>(`${commonPrescriptionPrefix}detail`, {
params: { id, type },
});
}
/**
* 常用方药品数据结构(西药/中成药)
* @description 西药药品的详细信息
*/
export interface CommonPrescriptionWestDrug {
/** 药品ID */
drug_id?: number;
/** 药品ID兼容字段 */
id?: number;
/** 药品名称 */
drug_name: string;
/** 每次用量 */
number?: number;
/** 购买数量 */
select_number?: number;
/** 单价 */
price?: number;
/** 使用时间ID */
time_id?: number;
/** 使用类型ID */
type_id?: number;
/** 使用频率ID */
frequency_id?: number;
/** 单位ID */
unit_id?: number;
/** 药品图片 */
image?: string;
/** 说明书 */
instruction?: string;
/** 使用时间信息 */
use_num?: { name?: string };
/** 使用类型信息 */
use_type?: { name?: string };
/** 使用频率信息 */
use_frequency?: { name?: string };
/** 单位信息 */
unit?: { name?: string };
}
/**
* 常用方药品数据结构(中药/颗粒药)
* @description 中药/颗粒药药品的详细信息
*/
export interface CommonPrescriptionChineseDrug {
/** 药品ID */
drug_id?: number;
/** 药品ID兼容字段 */
id?: number;
/** 药品名称 */
drug_name: string;
/** 克数/用量 */
number?: number;
/** 单价 */
price?: number;
/** 用法ID */
way_id?: number;
}
/**
* 保存西药常用方
* @description 将当前处方保存为西药常用方模板,后端会自动创建 recipe 记录
*/
export async function saveWestCommonPrescriptionApi(data: {
/** 常用方名称 */
name: string;
/** 药品详细信息数组 */
drugs: CommonPrescriptionWestDrug[];
/** 诊所ID */
store_id: number;
/** 临床诊断(可选) */
clinical_diagnose?: string;
/** 医嘱(可选) */
doctor_order?: string;
/** 类别 1-自费 2-医保(可选) */
category?: string;
}) {
return requestClient.post<any>(`${commonPrescriptionPrefix}save-west`, data);
}
/**
* 保存中药常用方
* @description 将当前处方保存为中药常用方模板,后端会自动创建 recipe 记录
*/
export async function saveChineseCommonPrescriptionApi(data: {
/** 常用方名称 */
name: string;
/** 药品详细信息数组 */
drugs: CommonPrescriptionChineseDrug[];
/** 诊所ID */
store_id: number;
/** 临床诊断(可选) */
clinical_diagnose?: string;
/** 医嘱(可选) */
doctor_order?: string;
/** 类别 1-自费 2-医保(可选) */
category?: string;
/** 剂量/天数可选默认7 */
dosage?: number;
/** 每日次数可选默认2 */
day_dosage?: number;
}) {
return requestClient.post<any>(
`${commonPrescriptionPrefix}save-chinese`,
data,
);
}
/**
* 保存颗粒药常用方
* @description 将当前处方保存为颗粒药常用方模板,后端会自动创建 recipe 记录
*/
export async function saveGranularCommonPrescriptionApi(data: {
/** 常用方名称 */
name: string;
/** 药品详细信息数组 */
drugs: CommonPrescriptionChineseDrug[];
/** 诊所ID */
store_id: number;
/** 临床诊断(可选) */
clinical_diagnose?: string;
/** 医嘱(可选) */
doctor_order?: string;
/** 类别 1-自费 2-医保(可选) */
category?: string;
/** 剂量/天数可选默认7 */
dosage?: number;
/** 每日次数可选默认2 */
day_dosage?: number;
}) {
return requestClient.post<any>(
`${commonPrescriptionPrefix}save-granular`,
data,
);
}

View File

@@ -0,0 +1,347 @@
<script lang="ts" setup>
/**
* 常用方选择弹窗组件
*
* @description 用于在开处方时快速选择已保存的常用方模板
* - 支持西药、中药、颗粒药三种类型
* - 选择后会自动填充药品列表
* @author 系统
* @date 2024
*/
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import {
Button,
Col,
Collapse,
CollapsePanel,
Empty,
message,
Row,
Spin,
Tag,
} from 'ant-design-vue';
import {
getCommonPrescriptionListApi,
type CommonPrescriptionData,
} from '#/views/doctor/doctor-reception/api';
// ==================== 响应式数据 ====================
/**
* 常用方数据
*/
const commonPrescriptionData = ref<CommonPrescriptionData | null>(null);
/**
* 加载状态
*/
const loading = ref(false);
/**
* 当前选中的药品类型
* 1-西药2-中药3-颗粒药
*/
const currentType = ref(1);
/**
* 回调函数:选择常用方后的回调
*/
let onSelectCallback: ((data: any, type: number) => void) | null = null;
// ==================== Modal 配置 ====================
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
footer: false,
onCancel() {
modalApi.close();
},
onOpenChange(isOpen: boolean) {
if (isOpen) {
// 获取传入的参数
const data = modalApi.getData<{
type?: number;
onSelect?: (data: any, type: number) => void;
}>();
if (data?.type) {
currentType.value = data.type;
}
if (data?.onSelect) {
onSelectCallback = data.onSelect;
}
// 加载常用方列表
loadCommonPrescriptionList();
}
},
});
// ==================== 方法定义 ====================
/**
* 加载常用方列表
*/
async function loadCommonPrescriptionList() {
loading.value = true;
try {
const res = await getCommonPrescriptionListApi();
commonPrescriptionData.value = res;
} catch (error) {
console.error('获取常用方列表失败:', error);
message.error('获取常用方列表失败');
} finally {
loading.value = false;
}
}
/**
* 选择常用方
* @param prescription 常用方信息
* @param recipes 药品列表
* @param type 类型1-西药2-中药3-颗粒药
*/
function handleSelectPrescription(
prescription: any,
recipes: any[],
type: number,
) {
// 调用回调函数,将选中的常用方数据传递给父组件
if (onSelectCallback) {
onSelectCallback(
{
prescription,
recipes,
},
type,
);
}
message.success('已选择常用方,药品已填充到处方中');
modalApi.close();
}
/**
* 获取药品名称列表(用于展示)
* @param recipes 药品列表
* @param nameField 名称字段
* @param showNumber 是否显示克数(中药使用)
*/
function getDrugNames(recipes: any[], nameField = 'drug_name', showNumber = false): string {
if (!recipes || recipes.length === 0) return '暂无药品';
return recipes.map((item) => {
const name = item[nameField] || item.name;
if (showNumber && item.number) {
return `${name}(${item.number}g)`;
}
return name;
}).join('、');
}
/**
* 获取药品数量
* @param recipes 药品列表
*/
function getDrugCount(recipes: any[]): number {
return recipes?.length || 0;
}
</script>
<template>
<Modal class="w-[70%]" title="选择常用方">
<Page>
<Spin :spinning="loading">
<div v-if="!commonPrescriptionData" class="py-8 text-center">
<Empty description="加载中..." />
</div>
<div v-else>
<Row :gutter="16">
<!-- 西药常用方 - 只在西药Tab时显示 -->
<Col v-if="currentType === 2" :span="24" class="mb-6">
<div class="rounded-lg border border-border p-4 mb-4">
<h3 class="flex items-center gap-2 mb-4 text-base font-semibold">
<Tag color="blue">西药</Tag>
西药常用方
<span class="text-sm font-normal text-muted-foreground"
>({{
commonPrescriptionData.west_prescription?.length || 0
}})</span
>
</h3>
<div
v-if="
commonPrescriptionData.west_prescription &&
commonPrescriptionData.west_prescription.length > 0
"
>
<Collapse>
<CollapsePanel
v-for="(item, index) in commonPrescriptionData.west_prescription"
:key="item.id"
>
<template #header>
<div class="flex items-center justify-between w-full">
<span class="font-medium">{{
item.name || `西药处方 ${index + 1}`
}}</span>
<Tag color="blue" class="ml-auto mr-4"
>{{
getDrugCount(commonPrescriptionData.west[index])
}}种药品</Tag
>
</div>
</template>
<div>
<p v-if="item.clinical_diagnose" class="mb-2 leading-relaxed">
<span class="font-medium text-muted-foreground">临床诊断:</span>
<span>{{ item.clinical_diagnose }}</span>
</p>
<p v-if="item.doctor_order" class="mb-2 leading-relaxed">
<span class="font-medium text-muted-foreground">医嘱:</span>
<span>{{ item.doctor_order }}</span>
</p>
<p class="mb-2 leading-relaxed">
<span class="font-medium text-muted-foreground">药品:</span>
<span class="text-primary">{{
getDrugNames(commonPrescriptionData.west[index])
}}</span>
</p>
<div class="flex justify-end mt-4 pt-4 border-t border-border">
<Button
type="primary"
@click="
handleSelectPrescription(
item,
commonPrescriptionData.west[index],
1,
)
"
>
使用此常用方
</Button>
</div>
</div>
</CollapsePanel>
</Collapse>
</div>
<Empty v-else description="暂无西药常用方" />
</div>
</Col>
<!-- 中药常用方 - 只在中药Tab时显示 -->
<Col v-if="currentType === 1" :span="24" class="mb-6">
<div class="rounded-lg border border-border p-4 mb-4">
<h3 class="flex items-center gap-2 mb-4 text-base font-semibold">
<Tag color="green">中药</Tag>
中药常用方
<span class="text-sm font-normal text-muted-foreground"
>({{
commonPrescriptionData.chin_prescription?.length || 0
}})</span
>
</h3>
<div
v-if="
commonPrescriptionData.chin_prescription &&
commonPrescriptionData.chin_prescription.length > 0
"
>
<Collapse>
<CollapsePanel
v-for="(
item, index
) in commonPrescriptionData.chin_prescription"
:key="item.id"
>
<template #header>
<div class="flex items-center justify-between w-full">
<span class="font-medium">{{
item.name || `中药处方 ${index + 1}`
}}</span>
<Tag color="green" class="ml-auto mr-4"
>{{
getDrugCount(
commonPrescriptionData.chinese[index],
)
}}种药品</Tag
>
</div>
</template>
<div>
<p v-if="item.clinical_diagnose" class="mb-2 leading-relaxed">
<span class="font-medium text-muted-foreground">临床诊断:</span>
<span>{{ item.clinical_diagnose }}</span>
</p>
<p v-if="item.doctor_order" class="mb-2 leading-relaxed">
<span class="font-medium text-muted-foreground">医嘱:</span>
<span>{{ item.doctor_order }}</span>
</p>
<p class="mb-2 leading-relaxed">
<span class="font-medium text-muted-foreground">药品:</span>
<span class="text-primary">{{
getDrugNames(
commonPrescriptionData.chinese[index],
'drug_name',
true,
)
}}</span>
</p>
<div class="flex justify-end mt-4 pt-4 border-t border-border">
<Button
type="primary"
@click="
handleSelectPrescription(
item,
commonPrescriptionData.chinese[index],
2,
)
"
>
使用此常用方
</Button>
</div>
</div>
</CollapsePanel>
</Collapse>
</div>
<Empty v-else description="暂无中药常用方" />
</div>
</Col>
</Row>
</div>
</Spin>
</Page>
</Modal>
</template>
<style lang="scss" scoped>
:deep(.ant-collapse) {
background: transparent;
border: none;
.ant-collapse-item {
margin-bottom: 8px;
border-radius: 8px !important;
overflow: hidden;
}
.ant-collapse-content {
border-top: none;
}
}
</style>

View File

@@ -52,6 +52,8 @@ import PrescriptionDetail from './components/PrescriptionDetail.vue';
import WesternModal from './components/WesternModal.vue'; import WesternModal from './components/WesternModal.vue';
import RefusalOfTreatmentModal import RefusalOfTreatmentModal
from "#/views/doctor/doctor-reception/components/RefusalOfTreatmentModal.vue"; from "#/views/doctor/doctor-reception/components/RefusalOfTreatmentModal.vue";
// 常用方选择弹窗组件
import CommonPrescriptionModal from './components/CommonPrescriptionModal.vue';
interface Patient { interface Patient {
id: number; id: number;
@@ -320,6 +322,23 @@ const selectPatient = (patient: Patient, isUpdateTabType = true) => {
} }
} }
activePatient.value = patient.user_patient; activePatient.value = patient.user_patient;
// 恢复之前保存的 activeCategory
const savedCategory = localStorage.getItem(`activeCategory${patient.user_patient?.id}`);
if (savedCategory) {
activeCategory.value = Number.parseInt(savedCategory);
} else {
// 如果没有保存的值,检查两个存储键哪个有数据
const chineseData = localStorage.getItem(`prescriptionData_1_${patient.user_patient?.id}`);
const westData = localStorage.getItem(`prescriptionData_2_${patient.user_patient?.id}`);
if (chineseData && JSON.parse(chineseData).length > 0) {
activeCategory.value = 1;
} else if (westData && JSON.parse(westData).length > 0) {
activeCategory.value = 2;
}
// 否则保持默认值
}
getPatientItem(patient.id).then((value) => { getPatientItem(patient.id).then((value) => {
patientInfo.value = value; patientInfo.value = value;
userPatientHealthInquiry.value = value.user_patient_health_inquiry; userPatientHealthInquiry.value = value.user_patient_health_inquiry;
@@ -348,10 +367,41 @@ const decrement = (index: number) => {
} }
}; };
// 保存到本地存储 // 切换药品用法编辑状态
const toggleEditDrug = (index: number) => {
currentDrugs.value[index].isEditing = !currentDrugs.value[index].isEditing;
};
// 保存药品用法编辑
const saveEditDrug = (index: number) => {
const drug = currentDrugs.value[index];
// 更新 ID 字段
drug.type_id = drug.use_type?.id;
drug.frequency_id = drug.use_frequency?.id;
drug.time_id = drug.use_num?.id;
drug.unit_id = drug.unit?.id;
drug.isEditing = false;
updateLocalStorage();
};
// 更新药品用法字段
const updateDrugUsage = (index: number, field: string, value: any) => {
currentDrugs.value[index][field] = value;
};
/**
* 获取药品数据的存储key按类型分开存储
* @param patientId 患者ID
* @param category 类型1-中药2-西药
*/
const getStorageKey = (patientId: number | string | undefined, category: number) => {
return `prescriptionData_${category}_${patientId}`;
};
// 保存到本地存储(按类型分开存储)
const saveToLocalStorage = () => { const saveToLocalStorage = () => {
localStorage.setItem( localStorage.setItem(
`prescriptionData${activePatient.value?.id}`, getStorageKey(activePatient.value?.id, activeCategory.value),
JSON.stringify(currentDrugs.value), JSON.stringify(currentDrugs.value),
); );
}; };
@@ -364,11 +414,11 @@ const removeDrug = (index: number) => {
updateLocalStorage(); updateLocalStorage();
}; };
/** /**
* 修改缓存的处方信息 * 修改缓存的处方信息(按类型分开存储)
*/ */
const updateLocalStorage = () => { const updateLocalStorage = () => {
localStorage.setItem( localStorage.setItem(
`prescriptionData${activePatient.value?.id}`, getStorageKey(activePatient.value?.id, activeCategory.value),
JSON.stringify(currentDrugs.value), JSON.stringify(currentDrugs.value),
); );
getCurrentDrugs(); getCurrentDrugs();
@@ -376,11 +426,11 @@ const updateLocalStorage = () => {
const currentDrugs = ref([]); const currentDrugs = ref([]);
/** /**
* 获取缓存的处方信息 * 获取缓存的处方信息(按类型分开存储)
*/ */
const getCurrentDrugs = () => { const getCurrentDrugs = () => {
currentDrugs.value = JSON.parse( currentDrugs.value = JSON.parse(
localStorage.getItem(`prescriptionData${activePatient.value?.id}`) || '[]', localStorage.getItem(getStorageKey(activePatient.value?.id, activeCategory.value)) || '[]',
); );
}; };
getCurrentDrugs(); getCurrentDrugs();
@@ -606,6 +656,98 @@ const [PrescriptionDetailModal, PrescriptionDetailModalApi] = useVbenModal({
const [RefusalOfTreatmentModals, RefusalOfTreatmentModalApi] = useVbenModal({ const [RefusalOfTreatmentModals, RefusalOfTreatmentModalApi] = useVbenModal({
connectedComponent: RefusalOfTreatmentModal, connectedComponent: RefusalOfTreatmentModal,
}); });
// ==================== 常用方弹窗 ====================
const [CommonPrescriptionModals, CommonPrescriptionModalApi] = useVbenModal({
connectedComponent: CommonPrescriptionModal,
});
/**
* 打开常用方选择弹窗
* @description 打开常用方弹窗,选择后自动填充药品到处方中
*/
function openCommonPrescriptionModal() {
CommonPrescriptionModalApi.setData({
type: activeCategory.value,
onSelect: handleSelectCommonPrescription,
});
CommonPrescriptionModalApi.open();
}
/**
* 处理选择常用方
* @param data 常用方数据包含prescription和recipes
* @param type 类型1-西药2-中药3-颗粒药
* @description 选择常用方后,将药品列表填充到当前处方中
*/
function handleSelectCommonPrescription(data: any, type: number) {
const { prescription, recipes } = data;
// 根据类型处理不同的药品数据
if (type === 1) {
// 西药处方
recipes.forEach((recipe: any) => {
const newProduct = {
index_id: recipe.id,
id: recipe.id,
drug_name: recipe.drug_name,
number: recipe.number || 1,
use_num: recipe.use_time,
use_type: recipe.use_type,
use_frequency: recipe.use_frequency,
unit: recipe.west_unit,
price: recipe.price || 0,
time_id: recipe.time_id,
type_id: recipe.type_id,
frequency_id: recipe.frequency_id,
unit_id: recipe.unit_id,
image: recipe.image,
instruction: recipe.instruction,
type: recipe.type,
select_number: 1,
};
// 检查是否已存在
const existItem = currentDrugs.value.find(
(item) => item.id === recipe.id,
);
if (!existItem) {
currentDrugs.value.push(newProduct);
}
});
} else {
// 中药/颗粒药处方 - 使用 drug_id 作为唯一标识
recipes.forEach((recipe: any) => {
const drugId = recipe.drug_id || recipe.id;
const newProduct = {
index_id: drugId,
id: drugId,
drug_name: recipe.drug_name || recipe.name,
number: recipe.number || 1,
price: recipe.price || 0,
way_id: recipe.way_id || 0,
select_number: 1,
};
// 检查是否已存在 - 使用 drug_id 比对
const existItem = currentDrugs.value.find(
(item) => item.id === drugId,
);
if (!existItem) {
currentDrugs.value.push(newProduct);
}
});
}
// 更新诊断和医嘱
if (prescription.clinical_diagnose) {
diagnosis.value = prescription.clinical_diagnose;
}
if (prescription.doctor_order) {
medicalAdvice.value = prescription.doctor_order;
}
// 保存到本地存储
updateLocalStorage();
}
const openWesternModal = () => { const openWesternModal = () => {
// 打开西药处方模态框逻辑 // 打开西药处方模态框逻辑
WesternDrugModalApi.setData({ WesternDrugModalApi.setData({
@@ -632,14 +774,28 @@ function splitString(str: string) {
} }
/** /**
* 切换tab * 切换tab(中药/西药切换)
* @param id * @param id 类型1-中药2-西药
* @description 切换Tab时
* 1. 先保存当前Tab的药品数据
* 2. 更新activeCategory
* 3. 加载新Tab对应的药品数据
*/ */
function tabChange(id) { function tabChange(id) {
// 先保存当前Tab的药品数据
localStorage.setItem(
getStorageKey(activePatient.value?.id, activeCategory.value),
JSON.stringify(currentDrugs.value),
);
// 更新Tab类型
localStorage.setItem(`activeCategory${activePatient.value?.id}`, id); localStorage.setItem(`activeCategory${activePatient.value?.id}`, id);
currentDrugs.value = [];
updateLocalStorage();
activeCategory.value = id; activeCategory.value = id;
// 加载新Tab对应的药品数据
currentDrugs.value = JSON.parse(
localStorage.getItem(getStorageKey(activePatient.value?.id, id)) || '[]',
);
} }
/** /**
@@ -887,7 +1043,7 @@ const selectChineseId = ref(0);
*/ */
function selectOldDrugInfo(id) { function selectOldDrugInfo(id) {
const check = JSON.parse( const check = JSON.parse(
localStorage.getItem(`prescriptionData${activePatient.value?.id}`) || '[]', localStorage.getItem(getStorageKey(activePatient.value?.id, activeCategory.value)) || '[]',
).find((v) => v.id === id); ).find((v) => v.id === id);
if (check) { if (check) {
currentDrugs.value[selectChineseIndex.value].id = selectChineseId.value; currentDrugs.value[selectChineseIndex.value].id = selectChineseId.value;
@@ -1392,9 +1548,17 @@ watch(
> >
添加商品 添加商品
</Button> </Button>
<!-- 常用方按钮 -->
<Button
type="primary"
:class="activeCategory !== 1 ? 'ml-3' : ''"
@click="openCommonPrescriptionModal"
>
选择常用方
</Button>
<RadioGroup <RadioGroup
v-model:value="category" v-model:value="category"
:class="activeCategory !== 1 ? 'ml-5' : ''" class="ml-5"
> >
<RadioButton :value="1">自费</RadioButton> <RadioButton :value="1">自费</RadioButton>
<RadioButton :value="2">医保</RadioButton> <RadioButton :value="2">医保</RadioButton>
@@ -1750,11 +1914,62 @@ watch(
<p> <p>
<span>药品名称:{{ drug.drug_name }}</span> <span>药品名称:{{ drug.drug_name }}</span>
</p> </p>
<p> <!-- 西药用法显示/编辑模式 -->
<p v-if="activeCategory !== 1 && !drug.isEditing">
<span>用法:{{ <span>用法:{{
`${drug.use_type?.name}${drug.use_frequency?.name}${drug.use_num?.name},每次${drug.number}${drug.unit?.name}` `${drug.use_type?.name}${drug.use_frequency?.name}${drug.use_num?.name},每次${drug.number}${drug.unit?.name}`
}}</span> }}</span>
</p> </p>
<div v-else-if="activeCategory !== 1 && drug.isEditing" class="edit-usage-form">
<span>用法:</span>
<Select
:value="drug.use_type?.id"
size="small"
style="width: 80px"
@change="(val) => updateDrugUsage(index, 'use_type', drugUseType.find(item => item.id === val))"
>
<SelectOption v-for="item in drugUseType" :key="item.id" :value="item.id">
{{ item.name }}
</SelectOption>
</Select>
<Select
:value="drug.use_frequency?.id"
size="small"
style="width: 100px; margin-left: 4px"
@change="(val) => updateDrugUsage(index, 'use_frequency', drugUseFrequency.find(item => item.id === val))"
>
<SelectOption v-for="item in drugUseFrequency" :key="item.id" :value="item.id">
{{ item.name }}
</SelectOption>
</Select>
<Select
:value="drug.use_num?.id"
size="small"
style="width: 80px; margin-left: 4px"
@change="(val) => updateDrugUsage(index, 'use_num', drugTime.find(item => item.id === val))"
>
<SelectOption v-for="item in drugTime" :key="item.id" :value="item.id">
{{ item.name }}
</SelectOption>
</Select>
<span style="margin-left: 4px">每次</span>
<InputNumber
v-model:value="drug.number"
:min="1"
size="small"
style="width: 60px; margin-left: 4px"
/>
<Select
:value="drug.unit?.id"
size="small"
style="width: 60px; margin-left: 4px"
@change="(val) => updateDrugUsage(index, 'unit', drugUnit.find(item => item.id === val))"
>
<SelectOption v-for="item in drugUnit" :key="item.id" :value="item.id">
{{ item.name }}
</SelectOption>
</Select>
</div>
</div> </div>
<div> <div>
<div v-if="activeCategory === 1" class="quantity-control"> <div v-if="activeCategory === 1" class="quantity-control">
@@ -1774,6 +1989,17 @@ watch(
</div> </div>
<span>{{ drug.price }}</span> <span>{{ drug.price }}</span>
<div> <div>
<!-- 西药编辑/保存按钮 -->
<Button
v-if="activeCategory !== 1 && !drug.isEditing"
type="link"
@click="toggleEditDrug(index)"
>编辑</Button>
<Button
v-if="activeCategory !== 1 && drug.isEditing"
type="link"
@click="saveEditDrug(index)"
>保存</Button>
<Button type="link" @click="removeDrug(index)">删除</Button> <Button type="link" @click="removeDrug(index)">删除</Button>
<Button <Button
v-if="drug.instruction !== ''" v-if="drug.instruction !== ''"
@@ -1929,6 +2155,8 @@ watch(
<DoctorOrderModals/> <DoctorOrderModals/>
<WesternDrugModal/> <WesternDrugModal/>
<PrescriptionDetailModal/> <PrescriptionDetailModal/>
<!-- 常用方选择弹窗 -->
<CommonPrescriptionModals/>
<Modal v-model:open="showNewDrugModal" @ok="newDrugModalOk"> <Modal v-model:open="showNewDrugModal" @ok="newDrugModalOk">
要把【{{ newDrugInfo.name }}】添加到清单吗? 要把【{{ newDrugInfo.name }}】添加到清单吗?
</Modal> </Modal>

View File

@@ -0,0 +1,272 @@
/**
* 患者管理相关API
*
* @description 患者管理模块的所有接口定义
* - 患者列表查询(到店/线上)
* - 患者详情查看
* - 挂号记录和处方记录查询
* - 患者备注管理
* - 群发消息功能
* @author 系统
* @date 2024
*/
import { requestClient } from '#/api/request';
const prefix = 'patient-management/';
// ==================== 类型定义 ====================
/**
* 患者列表项
*/
export interface PatientItem {
/** 患者ID */
id: number;
/** 就诊人ID */
patient_id: number;
/** 患者姓名 */
patient: string;
/** 头像URL */
avatar: string;
/** 年龄 */
age: number;
/** 性别1-男2-女 */
sex: number;
/** 手机号 */
mobile: string;
/** 最近接诊时间 */
recent_accept: string;
}
/**
* 分页响应数据结构
*/
export interface PageResponse<T> {
/** 当前页 */
page: number;
/** 每页数量 */
size: number;
/** 总页数 */
page_count: number;
/** 总数量 */
total: number;
/** 数据列表 */
items: T[];
}
/**
* 患者详情
*/
export interface PatientDetail {
/** 患者基本信息 */
patient: {
id: number;
user_id: number;
name: string;
sex: number;
id_card: string;
mobile: string;
};
/** 头像 */
avatar: string;
/** 年龄 */
age: number;
/** 备注 */
remark: string;
/** 分组 */
group: { id: number; name: string }[] | string;
}
/**
* 挂号记录项
*/
export interface RegisterItem {
/** 挂号ID */
id: number;
/** 订单编号 */
order_no: string;
/** 挂号序号 */
order_number: string;
/** 挂号费用 */
price: number;
/** 是否支付 */
is_pay: number;
/** 状态 */
status: number;
/** 状态文字 */
status_text: string;
/** 状态类型 */
status_type: string;
/** 医生 */
doctor: string;
/** 诊所 */
store: string;
/** 创建时间 */
created_at: string;
/** 支付时间 */
pay_time: string;
}
/**
* 处方记录项
*/
export interface PrescriptionItem {
/** 处方ID */
id: number;
/** 处方编号 */
prescription_no: string;
/** 处方类型 */
prescription_type: number;
/** 类型 */
type: number;
/** 状态 */
status: number;
/** 临床诊断 */
clinical_diagnose: string;
/** 患者信息 */
patient: {
id: number;
name: string;
sex: number;
age: number;
};
/** 创建时间 */
created_at: string;
}
// ==================== API 函数 ====================
/**
* 获取患者列表
* @description 获取当前医生名下的患者列表
* @param params 查询参数
* - status: 1-到店接诊2-线上接诊(可选)
* - name: 患者姓名搜索(可选)
* - sex: 性别筛选(可选)
* - pageSize: 每页数量(可选)
*/
export async function getPatientListApi(params?: {
status?: number;
name?: string;
sex?: number;
pageSize?: number;
}) {
return requestClient.get<PageResponse<PatientItem>>(`${prefix}list`, {
params,
});
}
/**
* 获取医生名下所有患者列表
* @description 获取当前医生名下的所有患者(不区分到店/线上)
* @param keyword 搜索关键字(可选)
*/
export async function getPatientListsApi(keyword?: string) {
return requestClient.get<PageResponse<PatientItem>>(`${prefix}patient-lists`, {
params: { keyword },
});
}
/**
* 获取患者详情
* @description 获取患者的详细信息,包括基本信息、备注、分组
* @param upId 就诊人ID
*/
export async function getPatientDetailApi(upId: number) {
return requestClient.get<PatientDetail>(`${prefix}detail`, {
params: { up_id: upId },
});
}
/**
* 获取患者挂号记录列表
* @description 获取指定患者的挂号记录
* @param upId 就诊人ID
* @param pageSize 每页数量(可选)
*/
export async function getPatientRegisterListApi(
upId: number,
pageSize?: number,
) {
return requestClient.get<PageResponse<RegisterItem>>(
`${prefix}register-list`,
{
params: { up_id: upId, pageSize },
},
);
}
/**
* 获取患者处方记录列表
* @description 获取指定患者的处方记录
* @param upId 就诊人ID
* @param pageSize 每页数量(可选)
*/
export async function getPatientPrescriptionListApi(
upId: number,
pageSize?: number,
) {
return requestClient.get<PageResponse<PrescriptionItem>>(
`${prefix}prescription-list`,
{
params: { up_id: upId, pageSize },
},
);
}
/**
* 添加患者备注
* @description 为指定患者添加备注
* @param upId 就诊人ID
* @param remark 备注内容
*/
export async function savePatientRemarkApi(upId: number, remark: string) {
return requestClient.post<any>(`${prefix}save-remark`, {
up_id: upId,
remark,
});
}
/**
* 更新患者备注
* @description 更新指定患者的备注内容
* @param upId 就诊人ID
* @param remark 备注内容
*/
export async function updatePatientRemarkApi(upId: number, remark: string) {
return requestClient.post<any>(`${prefix}update-remark`, {
up_id: upId,
remark,
});
}
/**
* 群发消息给患者
* @description 向多个患者发送消息
* @param content 消息内容
* @param upIds 患者ID列表逗号分隔
* @param sendAt 发送时间(可选,留空则立即发送)
*/
export async function sendMessageApi(
content: string,
upIds: string,
sendAt?: string,
) {
return requestClient.post<any>(`${prefix}send-message`, {
content,
up_ids: upIds,
send_at: sendAt,
});
}
/**
* 获取患者基础健康信息
* @description 获取患者填写的基础健康问卷信息
* @param upId 就诊人ID
*/
export async function getPatientBaseInfoApi(upId: number) {
return requestClient.get<any>(`${prefix}base-info`, {
params: { up_id: upId },
});
}

View File

@@ -0,0 +1,452 @@
<script lang="ts" setup>
/**
* 患者详情弹窗组件
*
* @description 展示患者的详细信息
* - 基本信息、备注、分组
* - 挂号记录列表
* - 处方记录列表
* @author 系统
* @date 2024
*/
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import {
Avatar,
Button,
Card,
Col,
Descriptions,
Empty,
Input,
message,
Row,
Spin,
Table,
Tabs,
TabPane,
Tag,
} from 'ant-design-vue';
import { UserOutlined } from '@ant-design/icons-vue';
import {
getPatientDetailApi,
getPatientRegisterListApi,
getPatientPrescriptionListApi,
savePatientRemarkApi,
updatePatientRemarkApi,
type PatientDetail,
type RegisterItem,
type PrescriptionItem,
} from '../api';
// ==================== 响应式数据 ====================
/**
* 患者ID
*/
const patientId = ref(0);
/**
* 患者姓名
*/
const patientName = ref('');
/**
* 患者详情数据
*/
const patientDetail = ref<PatientDetail | null>(null);
/**
* 挂号记录列表
*/
const registerList = ref<RegisterItem[]>([]);
/**
* 处方记录列表
*/
const prescriptionList = ref<PrescriptionItem[]>([]);
/**
* 加载状态
*/
const loading = ref(false);
/**
* 挂号记录加载状态
*/
const registerLoading = ref(false);
/**
* 处方记录加载状态
*/
const prescriptionLoading = ref(false);
/**
* 当前激活的Tab
*/
const activeTab = ref('info');
/**
* 备注编辑模式
*/
const remarkEditing = ref(false);
/**
* 备注内容
*/
const remarkContent = ref('');
/**
* 是否已有备注
*/
const hasRemark = ref(false);
// ==================== 表格列配置 ====================
/**
* 挂号记录表格列
*/
const registerColumns = [
{ title: '订单编号', dataIndex: 'order_no', key: 'order_no' },
{ title: '医生', dataIndex: 'doctor', key: 'doctor' },
{ title: '诊所', dataIndex: 'store', key: 'store' },
{ title: '费用', dataIndex: 'price', key: 'price' },
{
title: '状态',
dataIndex: 'status_text',
key: 'status_text',
},
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at' },
];
/**
* 处方记录表格列
*/
const prescriptionColumns = [
{ title: '处方编号', dataIndex: 'prescription_no', key: 'prescription_no' },
{
title: '类型',
dataIndex: 'prescription_type',
key: 'prescription_type',
customRender: ({ text }: { text: number }) => {
const typeMap: Record<number, string> = {
1: '中药处方',
2: '西药处方',
5: '服务包',
};
return typeMap[text] || '未知';
},
},
{ title: '诊断', dataIndex: 'clinical_diagnose', key: 'clinical_diagnose' },
{
title: '状态',
dataIndex: 'status',
key: 'status',
customRender: ({ text }: { text: number }) => {
const statusMap: Record<number, string> = {
0: '待审核',
1: '已通过',
2: '未通过',
3: '无需审核',
};
return statusMap[text] || '未知';
},
},
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at' },
];
// ==================== Modal 配置 ====================
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
footer: false,
onCancel() {
modalApi.close();
},
onOpenChange(isOpen: boolean) {
if (isOpen) {
const data = modalApi.getData<{
patientId: number;
patientName: string;
}>();
if (data) {
patientId.value = data.patientId;
patientName.value = data.patientName;
activeTab.value = 'info';
loadPatientDetail();
}
}
},
});
// ==================== 方法定义 ====================
/**
* 加载患者详情
*/
async function loadPatientDetail() {
loading.value = true;
try {
const res = await getPatientDetailApi(patientId.value);
patientDetail.value = res;
remarkContent.value = res.remark || '';
hasRemark.value = !!res.remark;
} catch (error) {
console.error('获取患者详情失败:', error);
message.error('获取患者详情失败');
} finally {
loading.value = false;
}
}
/**
* 加载挂号记录
*/
async function loadRegisterList() {
registerLoading.value = true;
try {
const res = await getPatientRegisterListApi(patientId.value);
registerList.value = res.items || [];
} catch (error) {
console.error('获取挂号记录失败:', error);
message.error('获取挂号记录失败');
} finally {
registerLoading.value = false;
}
}
/**
* 加载处方记录
*/
async function loadPrescriptionList() {
prescriptionLoading.value = true;
try {
const res = await getPatientPrescriptionListApi(patientId.value);
prescriptionList.value = res.items || [];
} catch (error) {
console.error('获取处方记录失败:', error);
message.error('获取处方记录失败');
} finally {
prescriptionLoading.value = false;
}
}
/**
* Tab切换处理
* @param key Tab键值
*/
function handleTabChange(key: string) {
activeTab.value = key;
if (key === 'register' && registerList.value.length === 0) {
loadRegisterList();
} else if (key === 'prescription' && prescriptionList.value.length === 0) {
loadPrescriptionList();
}
}
/**
* 保存备注
*/
async function saveRemark() {
if (!remarkContent.value.trim()) {
message.warning('请输入备注内容');
return;
}
try {
if (hasRemark.value) {
await updatePatientRemarkApi(patientId.value, remarkContent.value);
} else {
await savePatientRemarkApi(patientId.value, remarkContent.value);
}
message.success('备注保存成功');
remarkEditing.value = false;
hasRemark.value = true;
// 刷新详情
loadPatientDetail();
} catch (error) {
console.error('保存备注失败:', error);
message.error('保存备注失败');
}
}
/**
* 获取性别文字
* @param sex 性别值
*/
function getSexText(sex: number): string {
return sex === 1 ? '男' : sex === 2 ? '女' : '未知';
}
</script>
<template>
<Modal class="w-[70%]" :title="`患者详情 - ${patientName}`">
<Page>
<Spin :spinning="loading">
<Tabs v-model:activeKey="activeTab" @change="handleTabChange">
<!-- 基本信息 -->
<TabPane key="info" tab="基本信息">
<div v-if="patientDetail">
<Row :gutter="24">
<!-- 左侧头像和基本信息 -->
<Col :span="8">
<Card>
<div class="text-center">
<Avatar :size="100" :src="patientDetail.avatar">
<template #icon><UserOutlined /></template>
</Avatar>
<h3 class="mt-4 text-xl font-bold">
{{ patientDetail.patient?.name }}
</h3>
<div class="mt-2">
<Tag
:color="
patientDetail.patient?.sex === 1
? '#1890ff'
: '#eb2f96'
"
>
{{ getSexText(patientDetail.patient?.sex) }}
</Tag>
<Tag color="blue">{{ patientDetail.age }}</Tag>
</div>
</div>
</Card>
</Col>
<!-- 右侧详细信息 -->
<Col :span="16">
<Card title="患者信息">
<Descriptions :column="2" bordered>
<Descriptions.Item label="姓名">
{{ patientDetail.patient?.name }}
</Descriptions.Item>
<Descriptions.Item label="性别">
{{ getSexText(patientDetail.patient?.sex) }}
</Descriptions.Item>
<Descriptions.Item label="年龄">
{{ patientDetail.age }}
</Descriptions.Item>
<Descriptions.Item label="手机号">
{{ patientDetail.patient?.mobile || '未填写' }}
</Descriptions.Item>
<Descriptions.Item label="身份证" :span="2">
{{ patientDetail.patient?.id_card || '未填写' }}
</Descriptions.Item>
</Descriptions>
</Card>
<!-- 分组信息 -->
<Card title="分组信息" class="mt-4">
<div
v-if="
Array.isArray(patientDetail.group) &&
patientDetail.group.length > 0
"
>
<Tag
v-for="item in patientDetail.group"
:key="item.id"
color="blue"
class="mr-2 mb-2"
>
{{ item.name }}
</Tag>
</div>
<Empty v-else description="暂无分组" />
</Card>
<!-- 备注信息 -->
<Card title="备注" class="mt-4">
<div v-if="!remarkEditing">
<p v-if="patientDetail.remark">
{{ patientDetail.remark }}
</p>
<Empty v-else description="暂无备注" />
<Button
type="primary"
size="small"
class="mt-2"
@click="remarkEditing = true"
>
{{ hasRemark ? '编辑备注' : '添加备注' }}
</Button>
</div>
<div v-else>
<Input.TextArea
v-model:value="remarkContent"
:rows="4"
placeholder="请输入备注内容"
/>
<div class="mt-2">
<Button
type="primary"
size="small"
@click="saveRemark"
>
保存
</Button>
<Button
size="small"
class="ml-2"
@click="remarkEditing = false"
>
取消
</Button>
</div>
</div>
</Card>
</Col>
</Row>
</div>
<Empty v-else description="暂无数据" />
</TabPane>
<!-- 挂号记录 -->
<TabPane key="register" tab="挂号记录">
<Spin :spinning="registerLoading">
<Table
:columns="registerColumns"
:data-source="registerList"
:pagination="false"
row-key="id"
>
<template #emptyText>
<Empty description="暂无挂号记录" />
</template>
</Table>
</Spin>
</TabPane>
<!-- 处方记录 -->
<TabPane key="prescription" tab="处方记录">
<Spin :spinning="prescriptionLoading">
<Table
:columns="prescriptionColumns"
:data-source="prescriptionList"
:pagination="false"
row-key="id"
>
<template #emptyText>
<Empty description="暂无处方记录" />
</template>
</Table>
</Spin>
</TabPane>
</Tabs>
</Spin>
</Page>
</Modal>
</template>
<style lang="scss" scoped>
:deep(.ant-descriptions-item-label) {
width: 100px;
}
</style>

View File

@@ -0,0 +1,284 @@
<script lang="ts" setup>
/**
* 群发消息弹窗组件
*
* @description 向多个患者群发消息
* - 支持编辑消息内容
* - 支持定时发送
* @author 系统
* @date 2024
*/
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import {
Button,
DatePicker,
Form,
FormItem,
Input,
message,
Switch,
} from 'ant-design-vue';
import dayjs from 'dayjs';
import { sendMessageApi } from '../api';
// ==================== 响应式数据 ====================
/**
* 患者ID列表
*/
const patientIds = ref<number[]>([]);
/**
* 消息内容
*/
const messageContent = ref('');
/**
* 是否定时发送
*/
const isScheduled = ref(false);
/**
* 定时发送时间
*/
const scheduledTime = ref<dayjs.Dayjs | null>(null);
/**
* 发送中状态
*/
const sending = ref(false);
/**
* 成功回调函数
*/
let onSuccessCallback: (() => void) | null = null;
// ==================== Modal 配置 ====================
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
resetForm();
modalApi.close();
},
onConfirm: async () => {
await handleSend();
},
onOpenChange(isOpen: boolean) {
if (isOpen) {
const data = modalApi.getData<{
patientIds: number[];
onSuccess?: () => void;
}>();
if (data) {
patientIds.value = data.patientIds;
if (data.onSuccess) {
onSuccessCallback = data.onSuccess;
}
}
}
},
});
// ==================== 方法定义 ====================
/**
* 发送消息
*/
async function handleSend() {
// 校验
if (!messageContent.value.trim()) {
message.warning('请输入消息内容');
return;
}
if (patientIds.value.length === 0) {
message.warning('没有选择发送对象');
return;
}
if (isScheduled.value && !scheduledTime.value) {
message.warning('请选择定时发送时间');
return;
}
sending.value = true;
modalApi.setState({ confirmLoading: true });
try {
// 转换患者ID为逗号分隔的字符串
const upIds = patientIds.value.join(',');
// 定时发送时间格式化
const sendAt = isScheduled.value && scheduledTime.value
? scheduledTime.value.format('YYYY-MM-DD HH:mm:ss')
: undefined;
await sendMessageApi(messageContent.value, upIds, sendAt);
message.success(
isScheduled.value
? `消息已设置定时发送,将于 ${sendAt} 发送给 ${patientIds.value.length} 位患者`
: `消息已发送给 ${patientIds.value.length} 位患者`,
);
// 调用成功回调
if (onSuccessCallback) {
onSuccessCallback();
}
resetForm();
modalApi.close();
} catch (error) {
console.error('发送消息失败:', error);
message.error('发送消息失败,请重试');
} finally {
sending.value = false;
modalApi.setState({ confirmLoading: false });
}
}
/**
* 重置表单
*/
function resetForm() {
messageContent.value = '';
isScheduled.value = false;
scheduledTime.value = null;
}
/**
* 禁用过去的时间
* @param current 当前日期
*/
function disabledDate(current: dayjs.Dayjs) {
return current && current < dayjs().startOf('day');
}
/**
* 禁用过去的时间(分钟级别)
*/
function disabledTime() {
const now = dayjs();
return {
disabledHours: () => {
const hours = [];
for (let i = 0; i < now.hour(); i++) {
hours.push(i);
}
return hours;
},
};
}
</script>
<template>
<Modal class="w-[500px]" title="群发消息">
<Page>
<Form layout="vertical">
<!-- 发送对象提示 -->
<FormItem>
<div class="send-info">
将向 <span class="count">{{ patientIds.length }}</span> 位患者发送消息
</div>
</FormItem>
<!-- 消息内容 -->
<FormItem label="消息内容" required>
<Input.TextArea
v-model:value="messageContent"
:rows="6"
:maxlength="500"
show-count
placeholder="请输入要发送的消息内容..."
/>
</FormItem>
<!-- 定时发送开关 -->
<FormItem label="定时发送">
<Switch v-model:checked="isScheduled" />
<span class="ml-2 text-gray-500">
{{ isScheduled ? '开启定时发送' : '立即发送' }}
</span>
</FormItem>
<!-- 定时发送时间 -->
<FormItem v-if="isScheduled" label="发送时间" required>
<DatePicker
v-model:value="scheduledTime"
show-time
format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择发送时间"
:disabled-date="disabledDate"
class="w-full"
/>
</FormItem>
<!-- 提示信息 -->
<FormItem>
<div class="tips">
<p>提示</p>
<ul>
<li>消息将通过微信订阅消息发送给患者</li>
<li>请确保消息内容符合平台规范</li>
<li v-if="isScheduled">定时消息将在指定时间自动发送</li>
</ul>
</div>
</FormItem>
</Form>
</Page>
</Modal>
</template>
<style lang="scss" scoped>
.send-info {
padding: 12px 16px;
background: #e6f7ff;
border-radius: 8px;
font-size: 14px;
.dark & {
background: rgba(24, 144, 255, 0.1);
}
.count {
font-size: 18px;
font-weight: bold;
color: #1890ff;
}
}
.tips {
padding: 12px 16px;
background: #fffbe6;
border-radius: 8px;
font-size: 12px;
color: #ad8b00;
.dark & {
background: rgba(250, 173, 20, 0.1);
color: #d4b106;
}
p {
font-weight: bold;
margin-bottom: 8px;
}
ul {
margin: 0;
padding-left: 20px;
li {
margin-bottom: 4px;
}
}
}
</style>

View File

@@ -0,0 +1,455 @@
<script lang="ts" setup>
/**
* 患者管理页面
*
* @description 医生名下患者管理功能
* - 患者列表展示
* - 搜索筛选功能
* - 查看患者详情
* - 群发消息功能
* @author 系统
* @date 2024
*/
import { ref, onMounted } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import {
Avatar,
Button,
Card,
Col,
Empty,
Input,
message,
Pagination,
Row,
Select,
SelectOption,
Spin,
Table,
Tag,
} from 'ant-design-vue';
import {
ManOutlined,
SearchOutlined,
SendOutlined,
UserOutlined,
WomanOutlined,
} from '@ant-design/icons-vue';
import {
getPatientListApi,
type PatientItem,
type PageResponse,
} from './api';
import PatientDetailModal from './components/PatientDetailModal.vue';
import SendMessageModal from './components/SendMessageModal.vue';
// ==================== 响应式数据 ====================
/**
* 患者列表数据
*/
const patientList = ref<PatientItem[]>([]);
/**
* 分页信息
*/
const pagination = ref({
current: 1,
pageSize: 20,
total: 0,
});
/**
* 加载状态
*/
const loading = ref(false);
/**
* 搜索关键字
*/
const searchName = ref('');
/**
* 性别筛选
*/
const searchSex = ref<number | undefined>(undefined);
/**
* 选中的患者ID列表用于群发消息
*/
const selectedPatientIds = ref<number[]>([]);
// ==================== Modal 配置 ====================
/**
* 患者详情弹窗
*/
const [PatientDetailModals, PatientDetailModalApi] = useVbenModal({
connectedComponent: PatientDetailModal,
});
/**
* 群发消息弹窗
*/
const [SendMessageModals, SendMessageModalApi] = useVbenModal({
connectedComponent: SendMessageModal,
});
// ==================== 方法定义 ====================
/**
* 获取患者列表
* @description 根据搜索条件获取患者列表
*/
async function fetchPatientList() {
loading.value = true;
try {
const res = await getPatientListApi({
name: searchName.value || undefined,
sex: searchSex.value,
pageSize: pagination.value.pageSize,
});
patientList.value = res.items || [];
pagination.value.total = res.total || 0;
pagination.value.current = res.page || 1;
} catch (error) {
console.error('获取患者列表失败:', error);
message.error('获取患者列表失败');
} finally {
loading.value = false;
}
}
/**
* 搜索患者
* @description 触发搜索
*/
function handleSearch() {
pagination.value.current = 1;
fetchPatientList();
}
/**
* 重置搜索条件
*/
function handleReset() {
searchName.value = '';
searchSex.value = undefined;
pagination.value.current = 1;
fetchPatientList();
}
/**
* 分页变化
* @param page 页码
* @param pageSize 每页数量
*/
function handlePageChange(page: number, pageSize: number) {
pagination.value.current = page;
pagination.value.pageSize = pageSize;
fetchPatientList();
}
/**
* 打开患者详情弹窗
* @param patient 患者信息
*/
function openPatientDetail(patient: PatientItem) {
PatientDetailModalApi.setData({
patientId: patient.patient_id,
patientName: patient.patient,
});
PatientDetailModalApi.open();
}
/**
* 打开群发消息弹窗
*/
function openSendMessageModal() {
if (selectedPatientIds.value.length === 0) {
message.warning('请先选择要发送消息的患者');
return;
}
SendMessageModalApi.setData({
patientIds: selectedPatientIds.value,
onSuccess: () => {
selectedPatientIds.value = [];
},
});
SendMessageModalApi.open();
}
/**
* 选择/取消选择患者
* @param patientId 患者ID
*/
function toggleSelectPatient(patientId: number) {
const index = selectedPatientIds.value.indexOf(patientId);
if (index > -1) {
selectedPatientIds.value.splice(index, 1);
} else {
selectedPatientIds.value.push(patientId);
}
}
/**
* 全选/取消全选
*/
function toggleSelectAll() {
if (selectedPatientIds.value.length === patientList.value.length) {
selectedPatientIds.value = [];
} else {
selectedPatientIds.value = patientList.value.map((p) => p.patient_id);
}
}
/**
* 获取性别文字
* @param sex 性别值
*/
function getSexText(sex: number): string {
return sex === 1 ? '男' : sex === 2 ? '女' : '未知';
}
/**
* 获取性别颜色
* @param sex 性别值
*/
function getSexColor(sex: number): string {
return sex === 1 ? '#1890ff' : sex === 2 ? '#eb2f96' : '#999';
}
// ==================== 生命周期 ====================
onMounted(() => {
fetchPatientList();
});
</script>
<template>
<Page title="患者管理" auto-content-height>
<!-- 搜索区域 -->
<Card class="mb-4">
<Row :gutter="16" align="middle">
<Col :span="6">
<Input
v-model:value="searchName"
placeholder="请输入患者姓名"
allow-clear
@pressEnter="handleSearch"
>
<template #prefix>
<SearchOutlined />
</template>
</Input>
</Col>
<Col :span="4">
<Select
v-model:value="searchSex"
placeholder="选择性别"
allow-clear
class="w-full"
>
<SelectOption :value="1"></SelectOption>
<SelectOption :value="2"></SelectOption>
</Select>
</Col>
<Col :span="8">
<Button type="primary" @click="handleSearch">搜索</Button>
<Button class="ml-2" @click="handleReset">重置</Button>
</Col>
<Col :span="6" class="text-right">
<Button
type="primary"
:disabled="selectedPatientIds.length === 0"
@click="openSendMessageModal"
>
<template #icon><SendOutlined /></template>
群发消息 ({{ selectedPatientIds.length }})
</Button>
</Col>
</Row>
</Card>
<!-- 患者列表 -->
<Card>
<Spin :spinning="loading">
<div v-if="patientList.length === 0" class="py-12 text-center">
<Empty description="暂无患者数据" />
</div>
<div v-else>
<!-- 全选操作 -->
<div class="mb-4 flex items-center justify-between">
<Button size="small" @click="toggleSelectAll">
{{
selectedPatientIds.length === patientList.length
? '取消全选'
: '全选'
}}
</Button>
<span class="text-gray-500">
{{ pagination.total }} 位患者
</span>
</div>
<!-- 患者卡片列表 -->
<Row :gutter="[16, 16]">
<Col
v-for="patient in patientList"
:key="patient.patient_id"
:xs="24"
:sm="12"
:md="8"
:lg="6"
:xl="4"
>
<Card
hoverable
:class="{
'patient-card': true,
selected: selectedPatientIds.includes(patient.patient_id),
}"
@click="toggleSelectPatient(patient.patient_id)"
>
<div class="patient-card-content">
<!-- 头像 -->
<Avatar
:size="64"
:src="patient.avatar"
class="patient-avatar"
>
<template #icon><UserOutlined /></template>
</Avatar>
<!-- 基本信息 -->
<div class="patient-info">
<h4 class="patient-name">{{ patient.patient }}</h4>
<div class="patient-tags">
<Tag :color="getSexColor(patient.sex)">
<template #icon>
<ManOutlined v-if="patient.sex === 1" />
<WomanOutlined v-else-if="patient.sex === 2" />
</template>
{{ getSexText(patient.sex) }}
</Tag>
<Tag v-if="patient.age" color="blue"
>{{ patient.age }}</Tag
>
</div>
<p class="patient-mobile" v-if="patient.mobile">
{{ patient.mobile }}
</p>
</div>
<!-- 操作按钮 -->
<div class="patient-actions" @click.stop>
<Button
type="link"
size="small"
@click="openPatientDetail(patient)"
>
查看详情
</Button>
</div>
</div>
</Card>
</Col>
</Row>
<!-- 分页 -->
<div class="mt-4 text-right">
<Pagination
v-model:current="pagination.current"
v-model:pageSize="pagination.pageSize"
:total="pagination.total"
show-size-changer
show-quick-jumper
@change="handlePageChange"
/>
</div>
</div>
</Spin>
</Card>
<!-- 弹窗组件 -->
<PatientDetailModals />
<SendMessageModals />
</Page>
</template>
<style lang="scss" scoped>
.patient-card {
cursor: pointer;
transition: all 0.3s;
border: 2px solid transparent;
&.selected {
border-color: #1890ff;
background-color: #e6f7ff;
.dark & {
background-color: rgba(24, 144, 255, 0.1);
}
}
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
}
.patient-card-content {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.patient-avatar {
margin-bottom: 12px;
}
.patient-info {
width: 100%;
}
.patient-name {
font-size: 16px;
font-weight: 600;
margin-bottom: 8px;
color: #333;
.dark & {
color: #e5e7eb;
}
}
.patient-tags {
display: flex;
justify-content: center;
gap: 4px;
margin-bottom: 8px;
}
.patient-mobile {
font-size: 12px;
color: #999;
margin-bottom: 8px;
}
.patient-actions {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid #f0f0f0;
width: 100%;
.dark & {
border-top-color: #374151;
}
}
</style>

View File

@@ -1,6 +1,8 @@
import { requestClient } from '#/api/request'; import { requestClient } from '#/api/request';
const prefix = 'doctor/'; const prefix = 'doctor/';
const commonPrescriptionPrefix = 'common-prescription/';
/** /**
* 获取当前登录的医生信息 * 获取当前登录的医生信息
* @param data * @param data
@@ -8,6 +10,284 @@ const prefix = 'doctor/';
export async function getDoctorMyInfoApi(data: any) { export async function getDoctorMyInfoApi(data: any) {
return requestClient.get<any>(`${prefix}my-info`, { params: data }); return requestClient.get<any>(`${prefix}my-info`, { params: data });
} }
// ==================== 常用方管理 API ====================
/**
* 常用方列表响应数据结构
* @description 包含西药、中药、颗粒药三种类型的常用方
*/
export interface CommonPrescriptionResponse {
/** 西药常用方列表 */
west_prescription: any[];
/** 西药详细药品信息 */
west: any[][];
/** 中药常用方列表 */
chin_prescription: any[];
/** 中药详细药品信息 */
chinese: any[][];
/** 颗粒药常用方列表 */
granular_prescription: any[];
/** 颗粒药详细药品信息 */
granular: any[][];
}
/**
* 获取常用方列表
* @description 获取当前医生的所有常用方(西药/中药/颗粒药)
* @param storeId 诊所ID可选
*/
export async function getCommonPrescriptionListApi(storeId?: number) {
return requestClient.get<CommonPrescriptionResponse>(
`${commonPrescriptionPrefix}list`,
{ params: { store_id: storeId } },
);
}
/**
* 获取常用方详情
* @description 根据ID和类型获取常用方的详细信息
* @param id 常用方ID
* @param type 类型west-西药chinese-中药granular-颗粒药
*/
export async function getCommonPrescriptionDetailApi(
id: number,
type: string,
) {
return requestClient.get<any[]>(`${commonPrescriptionPrefix}detail`, {
params: { id, type },
});
}
/**
* 常用方药品数据结构(西药/中成药)
* @description 西药药品的详细信息
*/
export interface CommonPrescriptionWestDrug {
/** 药品ID */
drug_id?: number;
/** 药品ID兼容字段 */
id?: number;
/** 药品名称 */
drug_name: string;
/** 每次用量 */
number?: number;
/** 购买数量 */
select_number?: number;
/** 单价 */
price?: number;
/** 使用时间ID */
time_id?: number;
/** 使用类型ID */
type_id?: number;
/** 使用频率ID */
frequency_id?: number;
/** 单位ID */
unit_id?: number;
/** 药品图片 */
image?: string;
/** 说明书 */
instruction?: string;
/** 使用时间信息 */
use_num?: { name?: string };
/** 使用类型信息 */
use_type?: { name?: string };
/** 使用频率信息 */
use_frequency?: { name?: string };
/** 单位信息 */
unit?: { name?: string };
}
/**
* 常用方药品数据结构(中药/颗粒药)
* @description 中药/颗粒药药品的详细信息
*/
export interface CommonPrescriptionChineseDrug {
/** 药品ID */
drug_id?: number;
/** 药品ID兼容字段 */
id?: number;
/** 药品名称 */
drug_name: string;
/** 克数/用量 */
number?: number;
/** 单价 */
price?: number;
/** 用法ID */
way_id?: number;
}
/**
* 保存西药常用方
* @description 将当前处方保存为西药常用方模板,后端会自动创建 recipe 记录
* @param data 常用方数据
*/
export async function saveWestCommonPrescriptionApi(data: {
/** 常用方名称 */
name: string;
/** 药品详细信息数组 */
drugs: CommonPrescriptionWestDrug[];
/** 诊所ID */
store_id: number;
/** 临床诊断(可选) */
clinical_diagnose?: string;
/** 医嘱(可选) */
doctor_order?: string;
/** 类别 1-自费 2-医保(可选) */
category?: string;
}) {
return requestClient.post<any>(`${commonPrescriptionPrefix}save-west`, data);
}
/**
* 保存中药常用方
* @description 将当前处方保存为中药常用方模板,后端会自动创建 recipe 记录
* @param data 常用方数据
*/
export async function saveChineseCommonPrescriptionApi(data: {
/** 常用方名称 */
name: string;
/** 药品详细信息数组 */
drugs: CommonPrescriptionChineseDrug[];
/** 诊所ID */
store_id: number;
/** 临床诊断(可选) */
clinical_diagnose?: string;
/** 医嘱(可选) */
doctor_order?: string;
/** 类别 1-自费 2-医保(可选) */
category?: string;
/** 剂量/天数可选默认7 */
dosage?: number;
/** 每日次数可选默认2 */
day_dosage?: number;
}) {
return requestClient.post<any>(
`${commonPrescriptionPrefix}save-chinese`,
data,
);
}
/**
* 保存颗粒药常用方
* @description 将当前处方保存为颗粒药常用方模板,后端会自动创建 recipe 记录
* @param data 常用方数据
*/
export async function saveGranularCommonPrescriptionApi(data: {
/** 常用方名称 */
name: string;
/** 药品详细信息数组 */
drugs: CommonPrescriptionChineseDrug[];
/** 诊所ID */
store_id: number;
/** 临床诊断(可选) */
clinical_diagnose?: string;
/** 医嘱(可选) */
doctor_order?: string;
/** 类别 1-自费 2-医保(可选) */
category?: string;
/** 剂量/天数可选默认7 */
dosage?: number;
/** 每日次数可选默认2 */
day_dosage?: number;
}) {
return requestClient.post<any>(
`${commonPrescriptionPrefix}save-granular`,
data,
);
}
/**
* 删除常用方
* @description 删除指定的常用方
* @param id 常用方ID
* @param type 类型west-西药chinese-中药granular-颗粒药
*/
export async function deleteCommonPrescriptionApi(id: number, type: string) {
return requestClient.post<any>(`${commonPrescriptionPrefix}delete`, {
id,
type,
});
}
/**
* 常用方更新参数(基础)
* @description 更新常用方基本信息的参数
*/
export interface UpdateCommonPrescriptionBaseParams {
/** 常用方ID */
id: number;
/** 常用方名称(可选) */
name?: string;
/** 临床诊断(可选) */
clinical_diagnose?: string;
/** 医嘱(可选) */
doctor_order?: string;
}
/**
* 西药常用方更新参数
* @description 更新西药常用方,支持更新药品列表
*/
export interface UpdateWestCommonPrescriptionParams extends UpdateCommonPrescriptionBaseParams {
/** 药品数组(可选) */
drugs?: CommonPrescriptionWestDrug[];
}
/**
* 中药/颗粒药常用方更新参数
* @description 更新中药/颗粒药常用方,支持更新药品列表
*/
export interface UpdateChineseCommonPrescriptionParams extends UpdateCommonPrescriptionBaseParams {
/** 药品数组(可选) */
drugs?: CommonPrescriptionChineseDrug[];
/** 剂量/天数(可选) */
dosage?: number;
/** 每日次数(可选) */
day_dosage?: number;
}
/**
* 更新西药常用方
* @description 更新西药常用方的基本信息和药品列表
* @param data 更新参数
*/
export async function updateWestCommonPrescriptionApi(
data: UpdateWestCommonPrescriptionParams,
) {
return requestClient.post<any>(
`${commonPrescriptionPrefix}update-west`,
data,
);
}
/**
* 更新中药常用方
* @description 更新中药常用方的基本信息和药品列表
* @param data 更新参数
*/
export async function updateChineseCommonPrescriptionApi(
data: UpdateChineseCommonPrescriptionParams,
) {
return requestClient.post<any>(
`${commonPrescriptionPrefix}update-chinese`,
data,
);
}
/**
* 更新颗粒药常用方
* @description 更新颗粒药常用方的基本信息和药品列表
* @param data 更新参数
*/
export async function updateGranularCommonPrescriptionApi(
data: UpdateChineseCommonPrescriptionParams,
) {
return requestClient.post<any>(
`${commonPrescriptionPrefix}update-granular`,
data,
);
}
/** /**
* 获取当前登录的医生信息 * 获取当前登录的医生信息
*/ */

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,33 @@
<script lang="ts" setup> <script lang="ts" setup>
/**
* 医生设置页面
*
* @description 医生个人中心,包含个人资料、常用方、常用医嘱、常用诊断等功能
* @author 系统
* @date 2024
*/
import { ref } from 'vue'; import { ref } from 'vue';
import { EllipsisText, Page } from '@vben/common-ui'; import { EllipsisText, Page, useVbenModal } from '@vben/common-ui';
import { useUserStore } from '@vben/stores'; import { useUserStore } from '@vben/stores';
// 常用方编辑弹窗组件
import EditCommonPrescriptionModal from './components/EditCommonPrescriptionModal.vue';
// 常用方新增弹窗组件
import AddCommonPrescriptionModal from './components/AddCommonPrescriptionModal.vue';
import { import {
Button, Button,
Card, Card,
Collapse,
CollapsePanel,
Empty,
Input, Input,
InputGroup, InputGroup,
InputNumber, InputNumber,
Modal,
notification, notification,
Popconfirm,
RadioButton, RadioButton,
RadioGroup, RadioGroup,
Rate, Rate,
@@ -20,30 +37,37 @@ import {
import { import {
createDoctorOrderApi, createDoctorOrderApi,
deleteCommonPrescriptionApi,
getCommonPrescriptionListApi,
getDoctorMyDiseaseListApi, getDoctorMyDiseaseListApi,
getDoctorMyDoctorOrderApi, getDoctorMyDoctorOrderApi,
getDoctorMyInfoApi, getDoctorMyInfoApi,
saveServicePriceApi, saveServicePriceApi,
} from '#/views/doctor/settings/api'; } from '#/views/doctor/settings/api';
import type { CommonPrescriptionResponse } from '#/views/doctor/settings/api';
const userInfoStore = useUserStore(); const userInfoStore = useUserStore();
// 映射数据(示例) // 映射数据(示例)
const identityMap = { 1: '中医', 2: '西医' }; const identityMap = { 1: '中医', 2: '西医' };
const activeTabBar = ref(1); const activeTabBar = ref(1);
/**
* Tab栏配置
* - 1: 个人资料
* - 3: 常用方(新增)
* - 4: 常用医嘱
* - 5: 常用诊断
*/
const tabBar = [ const tabBar = [
{ {
value: 1, value: 1,
label: '个人资料', label: '个人资料',
}, },
// { {
// value: 2, value: 3,
// label: '我的处方', label: '常用方',
// }, },
// {
// value: 3,
// label: '常用方',
// },
{ {
value: 4, value: 4,
label: '常用医嘱', label: '常用医嘱',
@@ -98,27 +122,146 @@ const getDoctorMyDiseaseList = () => {
doctorMyDiseaseList.value = res; doctorMyDiseaseList.value = res;
}); });
}; };
// ==================== 常用方相关 ====================
/**
* 常用方数据
* @description 包含西药、中药、颗粒药三种类型的常用方列表
*/
const commonPrescriptionData = ref<CommonPrescriptionResponse | null>(null);
/**
* 常用方加载状态
*/
const commonPrescriptionLoading = ref(false);
/**
* 获取常用方列表
* @description 获取当前医生的所有常用方
*/
const getCommonPrescriptionList = async () => {
commonPrescriptionLoading.value = true;
try {
const res = await getCommonPrescriptionListApi();
commonPrescriptionData.value = res;
} catch (error) {
console.error('获取常用方列表失败:', error);
} finally {
commonPrescriptionLoading.value = false;
}
};
/**
* 删除常用方
* @param id 常用方ID
* @param type 类型west-西药chinese-中药granular-颗粒药
*/
const handleDeleteCommonPrescription = async (id: number, type: string) => {
try {
await deleteCommonPrescriptionApi(id, type);
notification.success({
message: '删除成功',
duration: 2,
});
// 刷新列表
getCommonPrescriptionList();
} catch (error) {
notification.error({
message: '删除失败',
duration: 2,
});
}
};
/**
* 获取药品名称列表(用于展示)
* @param recipes 药品列表
* @param nameField 名称字段
*/
const getDrugNames = (recipes: any[], nameField = 'drug_name') => {
if (!recipes || recipes.length === 0) return '暂无药品';
return recipes.map((item) => item[nameField] || item.name).join('、');
};
// ==================== 常用方编辑弹窗 ====================
const [EditCommonPrescriptionModals, EditCommonPrescriptionModalApi] =
useVbenModal({
connectedComponent: EditCommonPrescriptionModal,
});
// ==================== 常用方新增弹窗 ====================
const [AddCommonPrescriptionModals, AddCommonPrescriptionModalApi] =
useVbenModal({
connectedComponent: AddCommonPrescriptionModal,
});
/**
* 打开新增常用方弹窗
*/
const handleAddCommonPrescription = () => {
AddCommonPrescriptionModalApi.setData({
onSave: () => {
// 刷新列表
getCommonPrescriptionList();
},
});
AddCommonPrescriptionModalApi.open();
};
/**
* 打开编辑常用方弹窗
* @param prescription 常用方信息
* @param recipes 药品列表
* @param type 类型west-西药chinese-中药granular-颗粒药
*/
const handleEditCommonPrescription = (
prescription: any,
recipes: any[],
type: 'west' | 'chinese' | 'granular',
) => {
EditCommonPrescriptionModalApi.setData({
prescription,
recipes,
type,
onSave: () => {
// 刷新列表
getCommonPrescriptionList();
},
});
EditCommonPrescriptionModalApi.open();
};
/** /**
* 医嘱列表 * 医嘱列表
*/ */
const doctorOrder = ref([]); const doctorOrder = ref([]);
/**
* Tab切换事件处理
* @description 根据当前选中的Tab加载对应的数据
*/
const tabBarChange = () => { const tabBarChange = () => {
switch (activeTabBar.value) { switch (activeTabBar.value) {
case 1: { case 1: {
// 个人资料
getMyInfo(); getMyInfo();
break; break;
} }
case 2: {
break;
}
case 3: { case 3: {
// 常用方
getCommonPrescriptionList();
break; break;
} }
case 4: { case 4: {
// 常用医嘱
getDoctorMyDoctorOrder(); getDoctorMyDoctorOrder();
break; break;
} }
case 5: { case 5: {
// 常用诊断
getDoctorMyDiseaseList(); getDoctorMyDiseaseList();
break; break;
} }
@@ -335,7 +478,210 @@ const createDoctorOrder = () => {
</div> </div>
</Card> </Card>
<Card v-else-if="data && activeTabBar === 2" title="我的处方" /> <Card v-else-if="data && activeTabBar === 2" title="我的处方" />
<Card v-else-if="data && activeTabBar === 3" title="常用方" /> <!-- 常用方Tab内容 -->
<Card v-else-if="activeTabBar === 3">
<template #title>
<div class="flex items-center justify-between">
<span>常用方</span>
<Button type="primary" @click="handleAddCommonPrescription">
新增常用方
</Button>
</div>
</template>
<div v-if="commonPrescriptionLoading" class="text-center py-8">
加载中...
</div>
<div v-else-if="!commonPrescriptionData" class="text-center py-8">
<Empty description="暂无数据" />
</div>
<div v-else>
<!-- 西药常用方 -->
<div
v-if="
commonPrescriptionData.west_prescription &&
commonPrescriptionData.west_prescription.length > 0
"
class="mb-6"
>
<h3 class="mb-4 border-l-4 border-blue-500 pl-3 text-lg font-semibold">
西药常用方
</h3>
<Collapse>
<CollapsePanel
v-for="(item, index) in commonPrescriptionData.west_prescription"
:key="item.id"
:header="item.name || `西药处方 ${index + 1}`"
>
<div class="space-y-2">
<p v-if="item.clinical_diagnose">
<span class="font-medium">临床诊断:</span>
{{ item.clinical_diagnose }}
</p>
<p v-if="item.doctor_order">
<span class="font-medium">医嘱:</span>{{ item.doctor_order }}
</p>
<p>
<span class="font-medium">药品:</span>
{{ getDrugNames(commonPrescriptionData.west[index]) }}
</p>
<div class="mt-4 flex justify-end gap-2">
<Button
type="primary"
size="small"
@click="
handleEditCommonPrescription(
item,
commonPrescriptionData.west[index],
'west',
)
"
>
编辑
</Button>
<Popconfirm
title="确定要删除这个常用方吗?"
ok-text="确定"
cancel-text="取消"
@confirm="handleDeleteCommonPrescription(item.id, 'west')"
>
<Button type="primary" danger size="small">删除</Button>
</Popconfirm>
</div>
</div>
</CollapsePanel>
</Collapse>
</div>
<!-- 中药常用方 -->
<div
v-if="
commonPrescriptionData.chin_prescription &&
commonPrescriptionData.chin_prescription.length > 0
"
class="mb-6"
>
<h3 class="mb-4 border-l-4 border-green-500 pl-3 text-lg font-semibold">
中药常用方
</h3>
<Collapse>
<CollapsePanel
v-for="(item, index) in commonPrescriptionData.chin_prescription"
:key="item.id"
:header="item.name || `中药处方 ${index + 1}`"
>
<div class="space-y-2">
<p v-if="item.clinical_diagnose">
<span class="font-medium">临床诊断:</span>
{{ item.clinical_diagnose }}
</p>
<p v-if="item.doctor_order">
<span class="font-medium">医嘱:</span>{{ item.doctor_order }}
</p>
<p>
<span class="font-medium">药品:</span>
{{ getDrugNames(commonPrescriptionData.chinese[index], 'name') }}
</p>
<div class="mt-4 flex justify-end gap-2">
<Button
type="primary"
size="small"
@click="
handleEditCommonPrescription(
item,
commonPrescriptionData.chinese[index],
'chinese',
)
"
>
编辑
</Button>
<Popconfirm
title="确定要删除这个常用方吗?"
ok-text="确定"
cancel-text="取消"
@confirm="handleDeleteCommonPrescription(item.id, 'chinese')"
>
<Button type="primary" danger size="small">删除</Button>
</Popconfirm>
</div>
</div>
</CollapsePanel>
</Collapse>
</div>
<!-- 颗粒药常用方 -->
<div
v-if="
commonPrescriptionData.granular_prescription &&
commonPrescriptionData.granular_prescription.length > 0
"
class="mb-6"
>
<h3 class="mb-4 border-l-4 border-orange-500 pl-3 text-lg font-semibold">
颗粒药常用方
</h3>
<Collapse>
<CollapsePanel
v-for="(item, index) in commonPrescriptionData.granular_prescription"
:key="item.id"
:header="item.name || `颗粒药处方 ${index + 1}`"
>
<div class="space-y-2">
<p v-if="item.clinical_diagnose">
<span class="font-medium">临床诊断:</span>
{{ item.clinical_diagnose }}
</p>
<p v-if="item.doctor_order">
<span class="font-medium">医嘱:</span>{{ item.doctor_order }}
</p>
<p>
<span class="font-medium">药品:</span>
{{ getDrugNames(commonPrescriptionData.granular[index], 'name') }}
</p>
<div class="mt-4 flex justify-end gap-2">
<Button
type="primary"
size="small"
@click="
handleEditCommonPrescription(
item,
commonPrescriptionData.granular[index],
'granular',
)
"
>
编辑
</Button>
<Popconfirm
title="确定要删除这个常用方吗?"
ok-text="确定"
cancel-text="取消"
@confirm="handleDeleteCommonPrescription(item.id, 'granular')"
>
<Button type="primary" danger size="small">删除</Button>
</Popconfirm>
</div>
</div>
</CollapsePanel>
</Collapse>
</div>
<!-- 无数据提示 -->
<div
v-if="
(!commonPrescriptionData.west_prescription ||
commonPrescriptionData.west_prescription.length === 0) &&
(!commonPrescriptionData.chin_prescription ||
commonPrescriptionData.chin_prescription.length === 0) &&
(!commonPrescriptionData.granular_prescription ||
commonPrescriptionData.granular_prescription.length === 0)
"
class="text-center py-8"
>
<Empty description="暂无常用方,请在开处方时保存常用方" />
</div>
</div>
</Card>
<Card v-else-if="doctorOrder && activeTabBar === 4" title="常用医嘱"> <Card v-else-if="doctorOrder && activeTabBar === 4" title="常用医嘱">
<div class="w-full"> <div class="w-full">
<InputGroup> <InputGroup>
@@ -366,6 +712,11 @@ const createDoctorOrder = () => {
{{ item.disease.name }} {{ item.disease.name }}
</Button> </Button>
</Card> </Card>
<!-- 常用方编辑弹窗 -->
<EditCommonPrescriptionModals />
<!-- 常用方新增弹窗 -->
<AddCommonPrescriptionModals />
</Page> </Page>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>