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