feat: 中药导入模块修复、部门财务的优化、队列的优化管理

This commit is contained in:
李琦
2026-08-07 13:17:07 +08:00
parent 9f37802ba7
commit 32eb96b4ad
19 changed files with 1317 additions and 604 deletions

View File

@@ -0,0 +1,658 @@
<script lang="ts" setup>
/**
* 中药药名气泡选择器
* - 对齐诊断/医嘱 EntryKeywordBubbleInput 聚焦弹出 Teleport 浮层
* - 远程搜索药品getProductListDoctorReception拼音/名称高亮
* - 选中后回传完整商品行,供接诊/复诊/特色方/常用方复用现有落库逻辑
*/
import {
computed,
nextTick,
onMounted,
onUnmounted,
ref,
watch,
} from 'vue';
import { LoadingOutlined, SearchOutlined } from '@ant-design/icons-vue';
import { useDebounceFn } from '@vueuse/core';
import { Empty, Input, Spin } from 'ant-design-vue';
import {
rankByMatchScore,
splitHighlight,
type HighlightPart,
} from '#/utils/dictSearchRank';
import { getProductListDoctorReception } from '#/views/doctor/doctor-reception/api';
export type ChineseDrugBubbleItem = {
id: number;
drug_id: number;
price: number;
buy_price?: number;
drug: Record<string, any>;
[key: string]: any;
};
const props = withDefaults(
defineProps<{
/** 已选药名(展示回填;打开搜索时仍可编辑关键词) */
displayName?: string;
/** 药品类型1 中药,颗粒等由调用方传入 */
type?: number;
storeId?: number;
registerId?: number;
placeholder?: string;
disabled?: boolean;
/** 卡片内紧凑宽度 */
compact?: boolean;
/** 额外 class便于焦点跳转 querySelector */
inputClass?: string;
}>(),
{
displayName: '',
type: 1,
storeId: 0,
registerId: 0,
placeholder: '药名',
disabled: false,
compact: false,
inputClass: '',
},
);
const emit = defineEmits<{
select: [item: ChineseDrugBubbleItem];
}>();
const ROW_HEIGHT = 44;
const HINT_HEIGHT = 34;
const HEAD_HEIGHT = 32;
const PANEL_PAD = 8;
const PREFERRED_PANEL = 320;
const containerRef = ref<HTMLElement | null>(null);
const panelRef = ref<HTMLElement | null>(null);
const listRef = ref<HTMLElement | null>(null);
const inputRef = ref<{ focus?: () => void } | null>(null);
const keyword = ref('');
const loading = ref(false);
const open = ref(false);
/** 是否处于「正在搜」true 时不把 displayName 强写回 keyword */
const searching = ref(false);
const highlightIndex = ref(0);
const options = ref<ChineseDrugBubbleItem[]>([]);
const scrollTop = ref(0);
const listViewportH = ref(200);
const panelStyle = ref<Record<string, string>>({});
const totalHeight = computed(() => options.value.length * ROW_HEIGHT);
const startIndex = computed(() =>
Math.max(0, Math.floor(scrollTop.value / ROW_HEIGHT) - 2),
);
const endIndex = computed(() => {
const visible = Math.ceil(listViewportH.value / ROW_HEIGHT) + 4;
return Math.min(options.value.length - 1, startIndex.value + visible);
});
const visibleRows = computed(() => {
const list = options.value;
const start = startIndex.value;
const end = endIndex.value;
if (list.length === 0 || end < start) return [];
return list.slice(start, end + 1).map((item, i) => ({
item,
index: start + i,
}));
});
const offsetY = computed(() => startIndex.value * ROW_HEIGHT);
/**
* 同步展示名:未打开搜索时用 displayName 回填输入框
*/
watch(
() => props.displayName,
(name) => {
if (!searching.value && !open.value) {
keyword.value = String(name || '');
}
},
{ immediate: true },
);
function partsOf(text: string): HighlightPart[] {
return splitHighlight(text, keyword.value.trim());
}
function drugNameOf(item: ChineseDrugBubbleItem): string {
return String(item?.drug?.drug_name || item?.drug_name || '').trim();
}
function unitLabelOf(item: ChineseDrugBubbleItem): string {
const unit = item?.drug?.unit || item?.unit;
if (typeof unit === 'object' && unit?.name) return String(unit.name);
return String(item?.drug?.specification || item?.specification || '').trim() || '-';
}
function priceLabelOf(item: ChineseDrugBubbleItem): string {
const p = Number(item?.price ?? 0);
return Number.isFinite(p) ? `¥${p.toFixed(2)}` : '-';
}
/**
* 面板 Teleport + fixed避免接诊/Modal overflow 裁切
*/
function updatePanelPlacement() {
const el = containerRef.value;
if (!el) return;
const rect = el.getBoundingClientRect();
const vw = window.innerWidth;
const vh = window.innerHeight;
const gap = 4;
const spaceBelow = vh - rect.bottom - PANEL_PAD;
const spaceAbove = rect.top - PANEL_PAD;
const placeTop = spaceBelow < 180 && spaceAbove > spaceBelow;
const avail = Math.max(140, placeTop ? spaceAbove : spaceBelow);
const panelMax = Math.min(PREFERRED_PANEL, avail);
listViewportH.value = Math.max(80, panelMax - HINT_HEIGHT - HEAD_HEIGHT);
const PANEL_MIN_WIDTH = 420;
const width = Math.min(
Math.max(rect.width, PANEL_MIN_WIDTH),
vw - PANEL_PAD * 2,
);
let left = rect.left;
if (left + width > vw - PANEL_PAD) {
left = vw - PANEL_PAD - width;
}
if (left < PANEL_PAD) left = PANEL_PAD;
const base: Record<string, string> = {
position: 'fixed',
zIndex: '4000',
width: `${width}px`,
maxHeight: `${panelMax}px`,
left: `${left}px`,
right: 'auto',
};
if (placeTop) {
panelStyle.value = {
...base,
top: 'auto',
bottom: `${Math.max(PANEL_PAD, vh - rect.top + gap)}px`,
};
} else {
panelStyle.value = {
...base,
top: `${rect.bottom + gap}px`,
bottom: 'auto',
};
}
}
/**
* 远程搜药;空关键词不请求(与原中药 Select 一致)
*/
async function fetchDrugs(kw: string) {
const name = String(kw || '').trim();
if (!name) {
options.value = [];
loading.value = false;
return;
}
loading.value = true;
try {
const res = await getProductListDoctorReception({
name,
type: props.type,
store_id: props.storeId || 0,
...(props.registerId > 0 ? { register_id: props.registerId } : {}),
});
const list = Array.isArray(res) ? res : [];
const mapped: ChineseDrugBubbleItem[] = list.map((item: any) => {
const drug = item.drug || {};
return {
...item,
id: Number(item.id || 0),
drug_id: Number(item.drug_id || drug.id || 0),
price: Number(item.price ?? 0),
buy_price: item.buy_price,
drug: {
...drug,
drug_name: drug.drug_name || item.drug_name || '',
},
};
});
options.value = rankByMatchScore(
mapped,
name,
(it) => drugNameOf(it),
(it) =>
[
it.drug?.pinyin,
it.drug?.pinyin_full,
it.drug?.initials_of_pinyin,
it.drug?.specification,
]
.map((v) => String(v || ''))
.join(' '),
);
highlightIndex.value = options.value.length > 0 ? 0 : -1;
scrollTop.value = 0;
if (listRef.value) listRef.value.scrollTop = 0;
} catch (e) {
console.error('中药药名搜索失败:', e);
options.value = [];
} finally {
loading.value = false;
nextTick(() => updatePanelPlacement());
}
}
const debouncedFetch = useDebounceFn((kw: string) => {
void fetchDrugs(kw);
}, 280);
function openPanel() {
if (props.disabled) return;
open.value = true;
searching.value = true;
nextTick(() => {
updatePanelPlacement();
if (keyword.value.trim()) {
debouncedFetch(keyword.value);
} else {
options.value = [];
}
});
}
function closePanel(restoreDisplay = true) {
open.value = false;
searching.value = false;
options.value = [];
highlightIndex.value = 0;
if (restoreDisplay) {
keyword.value = String(props.displayName || '');
}
}
function handleFocus() {
if (props.disabled) return;
// 聚焦时清空为搜索态,便于直接输入新药名
if (props.displayName && keyword.value === props.displayName) {
keyword.value = '';
}
openPanel();
}
function handleInput() {
searching.value = true;
if (!open.value) openPanel();
else updatePanelPlacement();
debouncedFetch(keyword.value);
}
function selectItem(item: ChineseDrugBubbleItem) {
const name = drugNameOf(item);
keyword.value = name;
searching.value = false;
open.value = false;
options.value = [];
emit('select', item);
}
/**
* 键盘高亮循环移动(与诊断 EntryKeywordBubble 一致:第一项再上键 → 最后一项)
*/
function moveHighlight(delta: number) {
if (options.value.length === 0) return;
const len = options.value.length;
let next = highlightIndex.value;
if (next < 0) next = 0;
else next = (next + delta + len) % len;
highlightIndex.value = next;
ensureRowVisible(highlightIndex.value);
}
function handleKeydown(e: KeyboardEvent) {
if (!open.value) {
if (e.key === 'ArrowDown' || e.key === 'Enter') {
e.preventDefault();
openPanel();
}
return;
}
if (e.key === 'Escape') {
e.preventDefault();
closePanel(true);
return;
}
if (options.value.length === 0) return;
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
e.preventDefault();
moveHighlight(1);
} else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
e.preventDefault();
moveHighlight(-1);
} else if (e.key === 'Enter') {
e.preventDefault();
const hit = options.value[highlightIndex.value];
if (hit) selectItem(hit);
}
}
function ensureRowVisible(index: number) {
const el = listRef.value;
if (!el || index < 0) return;
const top = index * ROW_HEIGHT;
const bottom = top + ROW_HEIGHT;
if (top < el.scrollTop) el.scrollTop = top;
else if (bottom > el.scrollTop + el.clientHeight) {
el.scrollTop = bottom - el.clientHeight;
}
// 同步虚拟列表切片(循环跳到首/尾时必须刷新可见区)
scrollTop.value = el.scrollTop;
}
function onListScroll() {
scrollTop.value = listRef.value?.scrollTop || 0;
}
function onDocPointerDown(e: MouseEvent) {
if (!open.value) return;
const t = e.target as Node;
if (containerRef.value?.contains(t)) return;
if (panelRef.value?.contains(t)) return;
closePanel(true);
}
function onWinChange() {
if (open.value) updatePanelPlacement();
}
onMounted(() => {
document.addEventListener('mousedown', onDocPointerDown, true);
window.addEventListener('resize', onWinChange);
window.addEventListener('scroll', onWinChange, true);
});
onUnmounted(() => {
document.removeEventListener('mousedown', onDocPointerDown, true);
window.removeEventListener('resize', onWinChange);
window.removeEventListener('scroll', onWinChange, true);
});
defineExpose({
focus() {
nextTick(() => {
const el = inputRef.value as any;
el?.focus?.();
const native = containerRef.value?.querySelector(
'input',
) as HTMLInputElement | null;
native?.focus();
});
},
close() {
closePanel(true);
},
});
</script>
<template>
<div
ref="containerRef"
class="chinese-drug-name-bubble"
:class="[{ 'chinese-drug-name-bubble--compact': compact }, inputClass]"
>
<Input
ref="inputRef"
v-model:value="keyword"
:placeholder="placeholder"
:disabled="disabled"
allow-clear
size="small"
@input="handleInput"
@focus="handleFocus"
@keydown="handleKeydown"
>
<template #prefix>
<LoadingOutlined v-if="loading" class="text-muted-foreground" />
<SearchOutlined v-else class="text-muted-foreground" />
</template>
</Input>
<Teleport to="body">
<div
v-if="open"
ref="panelRef"
class="chinese-drug-name-bubble__panel"
:style="panelStyle"
>
<div class="chinese-drug-name-bubble__hint">
<span> 循环切换 · Enter 选用 · Esc 关闭</span>
<span class="chinese-drug-name-bubble__count">
<template v-if="keyword.trim()">搜到 {{ options.length }} </template>
<template v-else>输入药名搜索</template>
</span>
</div>
<Spin :spinning="loading">
<Empty
v-if="!loading && options.length === 0"
:image="Empty.PRESENTED_IMAGE_SIMPLE"
:description="
keyword.trim() ? '未找到匹配药材' : '请输入药名/拼音搜索'
"
class="py-4"
/>
<template v-else>
<div
class="chinese-drug-name-bubble__head"
:class="{ 'has-score': !!keyword.trim() }"
>
<span class="col-name">药名</span>
<span class="col-spec">规格/单位</span>
<span class="col-price">价格</span>
<span v-if="keyword.trim()" class="col-score">匹配度</span>
</div>
<div
ref="listRef"
class="chinese-drug-name-bubble__list"
:style="{ height: `${listViewportH}px` }"
@scroll="onListScroll"
>
<div
class="chinese-drug-name-bubble__phantom"
:style="{ height: `${totalHeight}px` }"
/>
<div
class="chinese-drug-name-bubble__virtual"
:style="{ transform: `translateY(${offsetY}px)` }"
>
<div
v-for="{ item, index } in visibleRows"
:key="`${item.drug_id}_${index}`"
class="chinese-drug-name-bubble__row"
:class="{
'is-active': index === highlightIndex,
'has-score': !!keyword.trim(),
}"
:style="{ height: `${ROW_HEIGHT}px` }"
@mousedown.prevent="selectItem(item)"
@mouseenter="highlightIndex = index"
>
<span class="col-name" :title="drugNameOf(item)">
<template
v-for="(p, pi) in partsOf(drugNameOf(item))"
:key="`n_${pi}`"
>
<em v-if="p.hit" class="hit">{{ p.text }}</em>
<template v-else>{{ p.text }}</template>
</template>
</span>
<span class="col-spec" :title="unitLabelOf(item)">
{{ unitLabelOf(item) }}
</span>
<span class="col-price">{{ priceLabelOf(item) }}</span>
<span v-if="keyword.trim()" class="col-score">
{{ Number(item.match_score || 0) }}%
</span>
</div>
</div>
</div>
</template>
</Spin>
</div>
</Teleport>
</div>
</template>
<style scoped>
.chinese-drug-name-bubble {
position: relative;
display: inline-block;
vertical-align: middle;
box-sizing: border-box;
/* 锁死宽度:避免接诊/复诊/特色方/常用方 flex 布局把药名框撑满整行 */
width: 100px !important;
min-width: 88px !important;
max-width: 110px !important;
flex: 0 0 100px !important;
overflow: hidden;
}
.chinese-drug-name-bubble--compact {
width: 100px !important;
min-width: 88px !important;
max-width: 110px !important;
flex: 0 0 100px !important;
}
.chinese-drug-name-bubble :deep(.ant-input-affix-wrapper) {
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
padding-inline: 6px;
box-sizing: border-box;
}
.chinese-drug-name-bubble :deep(.ant-input-prefix) {
margin-inline-end: 2px;
}
.chinese-drug-name-bubble :deep(.ant-input) {
width: 100% !important;
min-width: 0 !important;
overflow: hidden;
text-overflow: ellipsis;
}
.chinese-drug-name-bubble__panel {
overflow: hidden;
display: flex;
flex-direction: column;
border-radius: 8px;
border: 1px solid hsl(var(--border));
background: hsl(var(--popover, var(--background)));
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.chinese-drug-name-bubble__hint {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 6px 12px;
font-size: 12px;
color: hsl(var(--muted-foreground));
border-bottom: 1px solid hsl(var(--border));
background: hsl(var(--muted) / 0.35);
}
.chinese-drug-name-bubble__count {
flex-shrink: 0;
font-weight: 600;
color: hsl(var(--foreground));
}
.chinese-drug-name-bubble__list {
position: relative;
overflow-y: auto;
overflow-x: hidden;
}
.chinese-drug-name-bubble__phantom {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: -1;
pointer-events: none;
}
.chinese-drug-name-bubble__virtual {
position: absolute;
top: 0;
left: 0;
right: 0;
}
.chinese-drug-name-bubble__head,
.chinese-drug-name-bubble__row {
display: grid;
grid-template-columns: 1fr 100px 72px;
gap: 8px;
padding: 6px 12px;
font-size: 13px;
align-items: center;
box-sizing: border-box;
}
.chinese-drug-name-bubble__head.has-score,
.chinese-drug-name-bubble__row.has-score {
grid-template-columns: 1fr 90px 64px 48px;
}
.chinese-drug-name-bubble__head {
flex-shrink: 0;
font-weight: 600;
color: hsl(var(--muted-foreground));
background: hsl(var(--popover, var(--background)));
border-bottom: 1px solid hsl(var(--border));
}
.chinese-drug-name-bubble__row {
cursor: pointer;
border-bottom: 1px solid hsl(var(--border) / 0.6);
}
.chinese-drug-name-bubble__row:hover,
.chinese-drug-name-bubble__row.is-active {
background: hsl(var(--accent));
color: hsl(var(--accent-foreground));
}
.col-name,
.col-spec {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.col-name {
font-weight: 500;
}
.col-spec {
font-size: 12px;
color: hsl(var(--muted-foreground));
}
.col-price {
text-align: right;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.col-score {
text-align: right;
font-variant-numeric: tabular-nums;
font-weight: 600;
color: #d4380d;
white-space: nowrap;
}
.chinese-drug-name-bubble__row.is-active .col-spec {
color: inherit;
opacity: 0.85;
}
.chinese-drug-name-bubble__row.is-active .col-score {
color: inherit;
}
.hit {
font-style: normal;
color: #cf1322;
background: rgba(255, 214, 102, 0.55);
border-radius: 2px;
padding: 0 1px;
}
</style>

View File

@@ -0,0 +1,5 @@
/**
* 中药药名气泡选择器
*/
export { default as ChineseDrugNameBubble } from './chinese-drug-name-bubble.vue';
export type { ChineseDrugBubbleItem } from './chinese-drug-name-bubble.vue';

View File

@@ -105,7 +105,9 @@ const downloadQRCode = async (name: string, isQr = false) => {
{{ doctorInfo.doctor.name }} {{ doctorInfo.doctor.title.name }}
</div>
<div class="qrAddress">
<span class="addressTitle">诊所地址</span>{{ previewAddress }}
<span class="addressTitle">{{
Number(doctorInfo?.store?.type) === 1 ? '药店地址:' : '诊所地址:'
}}</span>{{ previewAddress }}
</div>
<Image :preview="false" :src="doctorInfo.qr_code" class="mt-3" height="30" width="30" />
<h3 class="border-l-4 border-blue-500 pl-3 text-lg font-semibold">

View File

@@ -1,5 +1,9 @@
<script lang="ts" setup>
import { ref } from 'vue';
/**
* 诊所/药店二维码预览弹窗
* 按门店 type0 诊所 / 1 药店)切换「诊所|药店」文案,列表页与门店卡片共用
*/
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
@@ -11,6 +15,13 @@ const htmlToImage = ref('');
const url = ref('');
const previewTitle = ref('二维码');
const previewAddress = ref('');
/** 门店类型0=诊所 1=药店,未传时按诊所示 */
const storeType = ref(0);
const isPharmacy = computed(() => Number(storeType.value) === 1);
const kindLabel = computed(() => (isPharmacy.value ? '药店' : '诊所'));
const addressLabel = computed(() => `${kindLabel.value}地址:`);
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
footer: false,
@@ -28,21 +39,28 @@ const [Modal, modalApi] = useVbenModal({
},
onOpenChange(isOpen: boolean) {
if (isOpen) {
const { values, title, address } =
modalApi.getData<Record<string, any>>();
const { values, title, address, type, store_type } =
modalApi.getData<Record<string, any>>() || {};
if (values) {
url.value = values;
qrCodeUrl.value = values;
previewTitle.value = title;
previewAddress.value = address;
// 兼容 type / store_type 两种传参
storeType.value = Number(type ?? store_type ?? 0);
modalApi.setState({
confirmText: `保存${kindLabel.value}卡片图片`,
cancelText: `保存${kindLabel.value}二维码图片`,
});
}
}
},
});
/**
* 下载二维码
* @param name
* @param isQr
* 下载二维码卡片或纯二维码图片
* @param name 文件名后缀
* @param isQr true=只截二维码区域false=截整张卡片
*/
const downloadQRCode = async (name: string, isQr = false) => {
const element = isQr ? qrCodeUrl.value : htmlToImage.value;
@@ -51,12 +69,11 @@ const downloadQRCode = async (name: string, isQr = false) => {
allowTaint: true,
logging: false,
}).then((canvas) => {
// 创建a标签下载
const link = document.createElement('a'); // 创建a标签
link.href = canvas.toDataURL(); // 是canvas对象的一种方法用于将canvas对象转换为base64位编码
link.setAttribute('download', `${previewTitle.value}${name}.png`); // 利用了a标签的download 来下载 canvas图片
link.style.display = 'none'; // 将图片隐藏起来
document.body.append(link); // 插入到其中
const link = document.createElement('a');
link.href = canvas.toDataURL();
link.setAttribute('download', `${previewTitle.value}${name}.png`);
link.style.display = 'none';
document.body.append(link);
link.click();
});
};
@@ -69,7 +86,7 @@ const downloadQRCode = async (name: string, isQr = false) => {
<div class="qrTitle">{{ previewTitle }}</div>
<Image :preview="false" :src="url" height="30" width="30" />
<div class="qrAddress">
<span class="addressTitle">诊所地址</span>{{ previewAddress }}
<span class="addressTitle">{{ addressLabel }}</span>{{ previewAddress }}
</div>
</div>
</div>

View File

@@ -71,10 +71,13 @@ const stats = computed(() => cardData.value?.stats_summary ?? {});
const doctorList = computed(() => cardData.value?.doctor_list ?? []);
const isClinic = computed(() => Number(store.value?.type) === 0);
const isPharmacy = computed(() => Number(store.value?.type) === 1);
/** 按门店类型区分诊所/药店文案,避免药店仍显示「诊所」 */
const storeKindLabel = computed(() => (isPharmacy.value ? '药店' : '诊所'));
const qrTabLabel = computed(() => `${storeKindLabel.value}二维码`);
const modalTitle = computed(() => {
const name = store.value?.name;
if (!name) return '门店详情';
return isPharmacy.value ? `药店详情 · ${name}` : `诊所详情 · ${name}`;
return `${storeKindLabel.value}详情 · ${name}`;
});
const drugTypeOptions = [
@@ -479,7 +482,7 @@ function clinicTypeText(v: number) {
</div>
</Tabs.TabPane>
<Tabs.TabPane key="qr" tab="门店二维码">
<Tabs.TabPane key="qr" :tab="qrTabLabel">
<div class="flex flex-col items-center gap-4 py-6 pl-4">
<template v-if="cardData?.qr_code || store.qr_code">
<Image
@@ -488,13 +491,13 @@ function clinicTypeText(v: number) {
:preview="true"
/>
<div class="text-sm text-muted-foreground">
{{ store.name }} 门店二维码
{{ store.name }} {{ qrTabLabel }}
</div>
</template>
<template v-else>
<Empty description="尚未生成二维码" />
<Empty :description="`尚未生成${storeKindLabel}二维码`" />
<Button type="primary" :loading="generatingQr" @click="ensureQrCode">
生成二维码
生成{{ storeKindLabel }}二维码
</Button>
</template>
</div>

View File

@@ -94,6 +94,8 @@ export const usePrescriptionStore = defineStore('prescription', () => {
const newDrugInfo = ref<any>({});
const selectChineseIndex = ref(-1);
const selectChineseId = ref(0);
/** 中药气泡选中的完整商品行(添加时优先使用) */
const pendingNewChineseProduct = ref<any>(null);
// 二次签名相关
const doctorSecondSign = ref(0);
@@ -726,7 +728,91 @@ export const usePrescriptionStore = defineStore('prescription', () => {
updateCurrentDrugs(newDrugs);
};
// 新药品操作
/**
* 中药气泡:选中新增行药名
* @param item 商品检索完整行
*/
const onSelectNewChineseDrug = (item: any) => {
const drugId = Number(item?.drug_id || item?.drug?.id || 0);
if (!drugId) return;
const check = currentDrugs.value.find((v) => Number(v.id) === drugId);
if (check) {
newDrugInfo.value.id = '';
newDrugInfo.value.name = '';
pendingNewChineseProduct.value = null;
message.error('该药品已经在处方中了!');
return;
}
pendingNewChineseProduct.value = item;
newDrugInfo.value.id = drugId;
newDrugInfo.value.price = item.price;
newDrugInfo.value.name = item.drug?.drug_name || '';
newDrugInfo.value.unit =
item.drug?.unit ||
drugUnit.value.find((u) => u.id === item.drug?.unit_id);
newDrugInfo.value.unit_id = item.drug?.unit_id;
nextTick(() => {
const input = document.querySelector(
'.new-number-input input',
) as HTMLElement;
input?.focus();
});
};
/**
* 中药气泡:替换已选行药名
* @param index 行下标
* @param item 商品检索完整行
*/
const onSelectOldChineseDrug = (index: number, item: any) => {
const drugId = Number(item?.drug?.id || item?.drug_id || 0);
if (!drugId) return;
const check = currentDrugs.value.find(
(v, i) => i !== index && Number(v.id) === drugId,
);
if (check) {
message.error('该药品已经在处方中了!');
return;
}
const data = item;
const newProduct = {
index_id: data.id,
id: data.drug.id,
drug_name: data.drug.drug_name,
number: data.drug.number,
use_num: drugTime.value.find((u) => u.id === data.drug.time_id),
use_type: drugUseType.value.find((u) => u.id === data.drug.type_id),
use_frequency: drugUseFrequency.value.find(
(u) => u.id === data.drug.frequency_id,
),
unit:
data.drug?.unit ||
drugUnit.value.find((u) => u.id === data.drug.unit_id),
price: data.price,
buy_price: data.buy_price,
way_id: data.drug?.way_id,
use_ways: drugUseWay.value.find((u) => u.id === data.drug.way_id),
time_id: data.drug.time_id,
type_id: data.drug.type_id,
frequency_id: data.drug.frequency_id,
unit_id: data.drug.unit_id,
image: data.drug.image,
specification: data.drug?.specification || data.specification || '',
instruction: data.drug.instruction,
type: data.drug.type,
};
const newDrugs = [...currentDrugs.value];
newDrugs[index] = newProduct;
updateCurrentDrugs(newDrugs);
nextTick(() => {
const input = document.querySelector(
`.old-number-input-${index} input`,
) as HTMLElement;
input?.focus();
});
};
// 新药品操作Select 兼容)
const selectNewDrugInfo = () => {
const check = currentDrugs.value.find((v) => v.id === newDrugInfo.value.id);
if (check) {
@@ -737,6 +823,7 @@ export const usePrescriptionStore = defineStore('prescription', () => {
const data = drugList.value.find((v) => v.drug_id === newDrugInfo.value.id);
if (data) {
pendingNewChineseProduct.value = data;
newDrugInfo.value.price = data.price;
newDrugInfo.value.name = data.drug.drug_name;
// 选药后立刻带上真实单位,供新行数量后缀展示
@@ -822,7 +909,9 @@ export const usePrescriptionStore = defineStore('prescription', () => {
return false;
}
const data = drugList.value.find((v) => v.drug_id === newDrugInfo.value.id);
const data =
pendingNewChineseProduct.value ||
drugList.value.find((v) => v.drug_id === newDrugInfo.value.id);
if (!data) {
message.error('请选择药品');
return false;
@@ -833,6 +922,7 @@ export const usePrescriptionStore = defineStore('prescription', () => {
const success = addProducts(data);
if (success) {
newDrugInfo.value = {};
pendingNewChineseProduct.value = null;
}
return success;
};
@@ -1323,6 +1413,8 @@ export const usePrescriptionStore = defineStore('prescription', () => {
updateDrugUsage,
selectNewDrugInfo,
selectOldDrugInfo,
onSelectNewChineseDrug,
onSelectOldChineseDrug,
setSelectChineseIndex,
addDrugByChinese,
selectDrugByNewDrugInfo,

View File

@@ -68,6 +68,7 @@ import GoldenFormulaModal from '#/views/doctor/doctor-reception/components/Golde
import InfoModal from '#/components/modal/InfoModal.vue';
// 药品搜索选择组件
import { DrugSearchSelect } from '#/components/drug-search-select';
import { ChineseDrugNameBubble } from '#/components/chinese-drug-name-bubble';
import { formatPriceDisplay } from '#/utils/formatPrice';
import ChineseMedicineConfig from '#/views/doctor/doctor-reception/components/ChineseMedicineConfig.vue';
import { getDeliveryWarehouseOptionsByDrugs, getPrescriptionInfoApi } from '#/views/doctor/doctor-reception/api';
@@ -1408,69 +1409,53 @@ const cancelSaveCommonPrescription = () => {
<div>
<span class="card-index">{{ index + 1 }}</span>
<p>
<Select
v-model:value="drug.id"
:filter-option="false"
class="old-select-drug-name w-full mb-2"
<ChineseDrugNameBubble
:display-name="drug.drug_name"
:store-id="prescriptionStore.myStoreId"
:register-id="Number(prescriptionStore.currentRegisterId || 0)"
:type="1"
compact
input-class="old-select-drug-name"
placeholder="药名"
show-search
@change="prescriptionStore.selectOldDrugInfo"
@dropdown-visible-change="
prescriptionStore.setSelectChineseIndex(index, drug.id)
@select="
(item) =>
prescriptionStore.onSelectOldChineseDrug(index, item)
"
@search="searchOption"
/>
,
<InputNumber
v-model:value="drug.number"
:class="`old-number-input-${index}`"
:controls="false"
style="width: 80px; min-width: 80px"
@blur="prescriptionStore.updateChineseNumber"
@keydown="
prescriptionStore.updateChineseNumberGoNewDrug($event)
"
>
<template #addonAfter>{{ drug.unit?.name || 'g' }}</template>
</InputNumber>
,
<Select
:value="drug?.way_id"
placeholder="用法"
style="width: 88px; min-width: 72px"
@change="prescriptionStore.selectDrugUseWayChange"
@dropdown-visible-change="
prescriptionStore.selectProductChange(drug.index_id)
"
>
<SelectOption :value="0">煎服</SelectOption>
<SelectOption
v-if="prescriptionStore.drugList.length === 0"
:value="drug.id"
v-for="(
value, wayIndex
) in prescriptionStore.drugUseWay"
:key="wayIndex"
:value="value.id"
>
{{ drug.drug_name }}
</SelectOption>
<SelectOption
v-for="(value, drugIndex) in prescriptionStore.drugList"
v-else
:key="drugIndex"
:value="value.drug.id"
>
{{ value.drug.drug_name }}
{{ value.name }}
</SelectOption>
</Select>
<div class="flex gap-1 items-center">
<InputNumber
v-model:value="drug.number"
:class="`old-number-input-${index}`"
:controls="false"
style="width: 80px"
@blur="prescriptionStore.updateChineseNumber"
@keydown="
prescriptionStore.updateChineseNumberGoNewDrug($event)
"
>
<template #addonAfter>{{ drug.unit?.name || 'g' }}</template>
</InputNumber>
<Select
:value="drug?.way_id"
placeholder="用法"
style="flex:1"
@change="prescriptionStore.selectDrugUseWayChange"
@dropdown-visible-change="
prescriptionStore.selectProductChange(drug.index_id)
"
>
<SelectOption :value="0">煎服</SelectOption>
<SelectOption
v-for="(
value, wayIndex
) in prescriptionStore.drugUseWay"
:key="wayIndex"
:value="value.id"
>
{{ value.name }}
</SelectOption>
</Select>
</div>
</p>
<p class="card-price">
¥<span>{{ (drug.price * drug.number).toFixed(2) }}</span>
@@ -1498,58 +1483,49 @@ const cancelSaveCommonPrescription = () => {
<div>
<span class="card-index">{{ prescriptionStore.currentDrugs.length + 1 }}、</span>
<div>
<Select
v-model:value="prescriptionStore.newDrugInfo.id"
:filter-option="false"
class="new-select-drug-name w-full mb-2"
<ChineseDrugNameBubble
:display-name="prescriptionStore.newDrugInfo.name || ''"
:store-id="prescriptionStore.myStoreId"
:register-id="Number(prescriptionStore.currentRegisterId || 0)"
:type="1"
compact
input-class="new-select-drug-name"
placeholder="药名"
show-search
@change="prescriptionStore.selectNewDrugInfo"
@search="searchOption"
@select="prescriptionStore.onSelectNewChineseDrug"
/>
,
<InputNumber
v-model:value="prescriptionStore.newDrugInfo.number"
class="new-number-input"
style="width: 80px; min-width: 80px"
@blur="newDrugBlur"
@keydown="
prescriptionStore.selectDrugByNewDrugInfo($event, true)
"
>
<template #addonAfter>{{ prescriptionStore.newDrugInfo.unit?.name || 'g' }}</template>
</InputNumber>
,
<Select
v-model:value="prescriptionStore.newDrugInfo.way_id"
placeholder="用法"
style="width: 88px; min-width: 72px"
@keydown="
prescriptionStore.selectDrugByNewDrugInfo($event, true)
"
>
<SelectOption :value="0">煎服</SelectOption>
<SelectOption
v-for="(value, drugIndex) in prescriptionStore.drugList"
:key="drugIndex"
:value="value.drug_id"
v-for="(
value, wayIndex
) in prescriptionStore.drugUseWay"
:key="wayIndex"
:value="value.id"
>
{{ value.drug.drug_name }}
{{ value.name }}
</SelectOption>
</Select>
<div class="flex gap-1 items-center">
<InputNumber
v-model:value="prescriptionStore.newDrugInfo.number"
class="new-number-input"
style="width: 80px"
@blur="newDrugBlur"
@keydown="
prescriptionStore.selectDrugByNewDrugInfo($event, true)
"
>
<template #addonAfter>{{ prescriptionStore.newDrugInfo.unit?.name || 'g' }}</template>
</InputNumber>
<Select
v-model:value="prescriptionStore.newDrugInfo.way_id"
placeholder="用法"
style="flex:1"
@keydown="
prescriptionStore.selectDrugByNewDrugInfo($event, true)
"
>
<SelectOption :value="0">煎服</SelectOption>
<SelectOption
v-for="(
value, wayIndex
) in prescriptionStore.drugUseWay"
:key="wayIndex"
:value="value.id"
>
{{ value.name }}
</SelectOption>
</Select>
</div>
<p class="card-price">
¥<span>{{
(
@@ -2122,6 +2098,18 @@ const cancelSaveCommonPrescription = () => {
position: relative;
min-height: 120px;
}
/* 中药卡片:药名/克数/用法同一行,药名气泡锁宽 */
.prescription-card :deep(.chinese-drug-name-bubble) {
width: 100px !important;
min-width: 88px !important;
max-width: 110px !important;
flex: 0 0 100px !important;
vertical-align: middle;
}
.prescription-card > div > p,
.prescription-card > div > div {
white-space: nowrap;
}
.drug-categories {
display: flex;

View File

@@ -15,12 +15,9 @@ import {
Select,
SelectOption,
} from 'ant-design-vue';
import { debounce } from 'lodash-es';
import {
getDrugUseList,
getProductListDoctorReception,
} from '#/views/doctor/doctor-reception/api';
import { ChineseDrugNameBubble } from '#/components/chinese-drug-name-bubble';
import { getDrugUseList } from '#/views/doctor/doctor-reception/api';
import PrescriptionDiagnosisOrderCards from './PrescriptionDiagnosisOrderCards.vue';
@@ -118,7 +115,6 @@ const doctorOrderModel = computed({
});
const drugUseWay = ref<any[]>([]);
const chineseSearchResults = ref<any[]>([]);
const newDrugInfo = ref<{
id?: number;
name?: string;
@@ -151,48 +147,36 @@ function syncToParent() {
emit('update:dayDosage', dayDosageLocal.value);
}
const searchChineseDrugs = debounce(async (keyword: string) => {
if (!keyword || keyword.length < 1) {
chineseSearchResults.value = [];
return;
}
try {
const res = await getProductListDoctorReception({
name: keyword,
type: 1,
store_id: userStore.userInfo?.store_id || 2,
});
chineseSearchResults.value = Array.isArray(res) ? res : [];
} catch {
chineseSearchResults.value = [];
}
}, 300);
function selectNewDrug(drugId: number) {
/**
* 中药气泡:选中新增行
* @param selectedItem 商品检索完整行
*/
function selectNewDrug(selectedItem: any) {
const drugId = Number(selectedItem?.drug?.id || selectedItem?.drug_id || 0);
if (!drugId) return;
const exists = drugList.value.some(
(item) => item.id === drugId || item.drug_id === drugId,
);
if (exists) {
message.warning('该药品已在列表中');
newDrugInfo.value.id = undefined;
newDrugInfo.value.name = '';
return;
}
const selectedItem = chineseSearchResults.value.find(
(item) => item.drug?.id === drugId || item.drug_id === drugId,
);
if (selectedItem) {
newDrugInfo.value.name = selectedItem.drug?.drug_name || '';
newDrugInfo.value.price = selectedItem.price || 0;
newDrugInfo.value.way_id = 0;
newDrugInfo.value.unit =
selectedItem.drug?.unit || selectedItem.unit || null;
newDrugInfo.value.unit_id =
selectedItem.drug?.unit_id || selectedItem.unit_id || 0;
nextTick(() => {
const numberInput = document.querySelector('.new-chinese-number input') as HTMLInputElement;
numberInput?.focus();
});
}
newDrugInfo.value.id = drugId;
newDrugInfo.value.name = selectedItem.drug?.drug_name || '';
newDrugInfo.value.price = selectedItem.price || 0;
newDrugInfo.value.way_id = 0;
newDrugInfo.value.unit =
selectedItem.drug?.unit || selectedItem.unit || null;
newDrugInfo.value.unit_id =
selectedItem.drug?.unit_id || selectedItem.unit_id || 0;
nextTick(() => {
const numberInput = document.querySelector(
'.new-chinese-number input',
) as HTMLInputElement;
numberInput?.focus();
});
}
function addChineseDrug() {
@@ -216,11 +200,12 @@ function addChineseDrug() {
unit: newDrugInfo.value.unit || null,
});
newDrugInfo.value = {};
chineseSearchResults.value = [];
syncToParent();
message.success('已添加药品');
nextTick(() => {
const nameInput = document.querySelector('.new-chinese-name input') as HTMLInputElement;
const nameInput = document.querySelector(
'.new-chinese-name input',
) as HTMLInputElement;
nameInput?.focus();
});
}
@@ -234,34 +219,36 @@ function handleChineseKeydown(event: KeyboardEvent, isNew: boolean) {
}
}
function changeChineseDrug(index: number, drugId: number) {
/**
* 中药气泡:替换已有行
* @param index 行下标
* @param selectedItem 商品检索完整行
*/
function changeChineseDrug(index: number, selectedItem: any) {
const drugId = Number(selectedItem?.drug?.id || selectedItem?.drug_id || 0);
if (!drugId) return;
const exists = drugList.value.some(
(item, i) => i !== index && (item.id === drugId || item.drug_id === drugId),
);
if (exists) {
message.warning('该药品已在列表中');
const drug = drugList.value[index];
if (drug) drug.id = drug.drug_id;
return;
}
const selectedItem = chineseSearchResults.value.find(
(item) => item.drug?.id === drugId || item.drug_id === drugId,
);
if (selectedItem) {
const drug = drugList.value[index];
if (drug) {
drug.id = drugId;
drug.drug_id = drugId;
drug.drug_name = selectedItem.drug?.drug_name || '';
drug.price = selectedItem.price || 0;
drug.unit = selectedItem.drug?.unit || selectedItem.unit || null;
drug.unit_id = selectedItem.drug?.unit_id || selectedItem.unit_id || 0;
syncToParent();
nextTick(() => {
const numberInput = document.querySelector(`.chinese-number-${index} input`) as HTMLInputElement;
numberInput?.focus();
});
}
const drug = drugList.value[index];
if (drug) {
drug.id = drugId;
drug.drug_id = drugId;
drug.drug_name = selectedItem.drug?.drug_name || '';
drug.price = selectedItem.price || 0;
drug.unit = selectedItem.drug?.unit || selectedItem.unit || null;
drug.unit_id = selectedItem.drug?.unit_id || selectedItem.unit_id || 0;
syncToParent();
nextTick(() => {
const numberInput = document.querySelector(
`.chinese-number-${index} input`,
) as HTMLInputElement;
numberInput?.focus();
});
}
}
@@ -423,27 +410,15 @@ defineExpose({
<span>{{ getWayName(drug.way_id) }}</span>
</div>
<div v-else class="chinese-drug-form">
<Select
v-model:value="drug.id"
show-search
:filter-option="false"
<ChineseDrugNameBubble
:display-name="drug.drug_name"
:store-id="userStore.userInfo?.store_id || 0"
:type="1"
compact
input-class="chinese-drug-name"
placeholder="药名"
class="chinese-drug-name"
@search="searchChineseDrugs"
@change="(val) => changeChineseDrug(index, val as number)"
>
<SelectOption v-if="chineseSearchResults.length === 0" :value="drug.id">
{{ drug.drug_name }}
</SelectOption>
<SelectOption
v-for="item in chineseSearchResults"
v-else
:key="item.drug?.id || item.id"
:value="item.drug?.id || item.id"
>
{{ item.drug?.drug_name || item.drug_name }}
</SelectOption>
</Select>
@select="(item) => changeChineseDrug(index, item)"
/>
<span class="chinese-drug-comma"></span>
<InputNumber
v-model:value="drug.number"
@@ -523,23 +498,15 @@ defineExpose({
<div class="chinese-drug-content">
<span class="chinese-drug-index">{{ drugList.length + 1 }}</span>
<div class="chinese-drug-form">
<Select
v-model:value="newDrugInfo.id"
show-search
:filter-option="false"
<ChineseDrugNameBubble
:display-name="newDrugInfo.name || ''"
:store-id="userStore.userInfo?.store_id || 0"
:type="1"
compact
input-class="chinese-drug-name new-chinese-name"
placeholder="药名"
class="chinese-drug-name new-chinese-name"
@search="searchChineseDrugs"
@change="selectNewDrug"
>
<SelectOption
v-for="item in chineseSearchResults"
:key="item.drug?.id || item.id"
:value="item.drug?.id || item.id"
>
{{ item.drug?.drug_name || item.drug_name }}
</SelectOption>
</Select>
@select="selectNewDrug"
/>
<span class="chinese-drug-comma"></span>
<InputNumber
v-model:value="newDrugInfo.number"
@@ -607,15 +574,18 @@ defineExpose({
.chinese-drug-form {
display: flex;
flex-wrap: wrap;
flex-wrap: nowrap;
align-items: center;
gap: 4px;
padding-left: 28px;
overflow: hidden;
}
.chinese-drug-name {
min-width: 80px;
max-width: 120px;
/* 药名气泡锁宽,与克数/用法保持同一行 */
.chinese-drug-form :deep(.chinese-drug-name-bubble) {
width: 100px !important;
min-width: 88px !important;
max-width: 110px !important;
flex: 0 0 100px !important;
}
.chinese-drug-readonly {

View File

@@ -100,7 +100,7 @@ const [Modal, modalApi] = useVbenModal({
</script>
<template>
<Modal title="编辑药方" class="w-[80%] md:w-[50%] lg:w-[30%] h-[60%]">
<Modal title="编辑药方" class="w-[80%]">
<div v-if="prescriptionType === 'chinese'">
<ChineseDrugEditor
ref="chineseDrugEditorRef"

View File

@@ -67,7 +67,7 @@ const [Modal, modalApi] = useVbenModal({
</script>
<template>
<Modal :title="modalTitle" class="w-[420px]">
<Modal :title="modalTitle" class="w-[80%]">
<div class="free-shipping-dose-modal">
<p v-if="spName" class="sp-name">特色方{{ spName }}</p>
<div class="form-row">

View File

@@ -178,7 +178,7 @@ function bindPrescriptionTypeChange() {
</script>
<template>
<Modal :title="`${isUpdate ? '编辑' : '新增'}特色方`" class="w-[80%] md:w-[50%] lg:w-[30%] h-[60%]">
<Modal :title="`${isUpdate ? '编辑' : '新增'}特色方`" class="w-[80%]">
<Form />
<div v-if="prescriptionType === 'chinese'" class="mt-4 border-t pt-4">
<ChineseDrugEditor

View File

@@ -91,7 +91,9 @@ const downloadQRCode = async (name: string, isQr = false) => {
{{ data.qr_code.store.name }}
</div>
<div class="qrAddress">
<span class="addressTitle">诊所地址</span>{{ data.qr_code.store.position }}
<span class="addressTitle">{{
Number(data?.qr_code?.store?.type) === 1 ? '药店地址:' : '诊所地址:'
}}</span>{{ data.qr_code.store.position }}
</div>
<Image :preview="false" :src="data.qr_code.qr_code" class="mt-3" height="30" width="30" />
<div class="qrAddress">

View File

@@ -484,6 +484,21 @@ export async function aiMatchPrescriptionDrugsApi(data: {
return requestClient.post<any>(`${prefix}ai-match-prescription-drugs`, data);
}
/**
* AI 出方:改选对照药品并写回 result_json标记 is_modified
*/
export async function aiSavePrescriptionDrugSelectionApi(data: {
generation_id: number;
item_index: number;
selected_drug_id: number;
selected_drug_name?: string;
}) {
return requestClient.post<any>(
`${prefix}ai-save-prescription-drug-selection`,
data,
);
}
/** 金方列表VIPgolden_formula */
export async function goldenFormulaListApi(data: {
keyword?: string;

View File

@@ -13,6 +13,7 @@ import {
aiGeneratePrescriptionApi,
aiListGenerationsApi,
aiMatchPrescriptionDrugsApi,
aiSavePrescriptionDrugSelectionApi,
getProcessRuleList,
} from '../api';
import AiDisclaimerBanner from '#/views/doctor/components/AiDisclaimerBanner.vue';
@@ -509,6 +510,46 @@ async function onSelectHistory(row: any) {
}
}
/**
* 医生点选对照候选药:本地更新 + 同步写回 aijsonresult_json
* @param mi matchedList 下标
* @param candidate 候选药品
*/
async function onPickCandidate(mi: number, candidate: any) {
const row = matchedList.value[mi];
if (!row || !candidate) return;
const drugId = Number(candidate.drug_id || 0);
if (!drugId) return;
if (Number(row.selected_drug_id) === drugId) return;
const prevId = Number(row.selected_drug_id || 0);
const prevModified = Number(row.is_modified || 0);
row.selected_drug_id = drugId;
row.is_modified = 1;
const generationId = Number(activeId.value || 0);
const itemIndex = Number(
row.item_index !== undefined && row.item_index !== null
? row.item_index
: mi,
);
if (generationId <= 0) {
message.warning('请先生成或选择历史处方后再改选');
return;
}
try {
await aiSavePrescriptionDrugSelectionApi({
generation_id: generationId,
item_index: itemIndex,
selected_drug_id: drugId,
selected_drug_name: String(candidate.drug_name || ''),
});
} catch (e: any) {
// 回滚本地,避免 UI 与库不一致
row.selected_drug_id = prevId;
row.is_modified = prevModified;
message.error(e?.message || e?.msg || '改选保存失败');
}
}
function onConfirmImport() {
const drugs: any[] = [];
for (const m of matchedList.value) {
@@ -670,7 +711,16 @@ defineExpose({
>
<div class="flex items-start gap-1">
<div class="min-w-0 flex-1">
<div class="truncate text-xs font-medium">{{ m.ai_name }}</div>
<div class="truncate text-xs font-medium">
{{ m.ai_name }}
<Tag
v-if="Number(m.is_modified) === 1"
color="orange"
class="!m-0 !ml-1 !px-1 !text-[10px] !leading-4"
>
已改
</Tag>
</div>
<div class="text-[11px] text-muted-foreground">
{{ m.dose }}{{ m.unit }}{{ m.usage ? `·${m.usage}` : '' }}
</div>
@@ -687,7 +737,7 @@ defineExpose({
? 'success'
: undefined
"
@click="m.selected_drug_id = Number(c.drug_id)"
@click="onPickCandidate(mi, c)"
>
{{ c.drug_name }}
</Tag>
@@ -701,7 +751,16 @@ defineExpose({
class="rounded-lg border border-border bg-card p-2.5"
>
<div class="flex items-center gap-2">
<span class="min-w-0 flex-1 truncate font-medium">{{ m.ai_name }}</span>
<span class="min-w-0 flex-1 truncate font-medium">
{{ m.ai_name }}
<Tag
v-if="Number(m.is_modified) === 1"
color="orange"
class="!m-0 !ml-1 !text-[10px]"
>
已改
</Tag>
</span>
<span class="text-xs text-muted-foreground">
{{ m.dose }}{{ m.unit }}{{ m.usage ? `·${m.usage}` : '' }}
</span>
@@ -717,7 +776,7 @@ defineExpose({
? 'success'
: undefined
"
@click="m.selected_drug_id = Number(c.drug_id)"
@click="onPickCandidate(mi, c)"
>
{{ c.drug_name }} {{ Number(c.price || 0).toFixed(2) }}
</Tag>

View File

@@ -97,6 +97,8 @@ import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
import { detailDoctorOrderToText } from '#/utils/prescriptionDoctorOrder';
// 药品搜索选择组件
import { DrugSearchSelect } from '#/components/drug-search-select';
// 中药药名气泡(对齐诊断/医嘱浮层交互)
import { ChineseDrugNameBubble } from '#/components/chinese-drug-name-bubble';
// 保存常用方API
import {
saveChineseCommonPrescriptionApi,
@@ -2599,28 +2601,114 @@ const getDrugListByWesternModal = debounce(async (searchText = '') => {
// getDrugListByWesternModal();
/** 新行中药气泡选中的完整商品(添加时用,避免再依赖 drugList */
const pendingNewChineseProduct = ref<any>(null);
/**
* 选择药品
* 中药气泡:选中新增行药名
* @param item 商品检索完整行
*/
function onSelectNewChineseDrug(item: any) {
const drugId = Number(item?.drug_id || item?.drug?.id || 0);
if (!drugId) return;
const check = currentDrugs.value.find((v) => Number(v.id) === drugId);
if (check) {
newDrugInfo.value.id = '';
newDrugInfo.value.name = '';
pendingNewChineseProduct.value = null;
message.error('该药品已经在处方中了!');
return;
}
pendingNewChineseProduct.value = item;
newDrugInfo.value.id = drugId;
newDrugInfo.value.price = item.price;
newDrugInfo.value.name = item.drug?.drug_name || '';
newDrugInfo.value.unit =
item.drug?.unit ||
drugUnit.value.find((u) => u.id === item.drug?.unit_id);
newDrugInfo.value.unit_id = item.drug?.unit_id;
setTimeout(() => {
const newDrugNumberInput = document.querySelector(
'.new-number-input input',
);
if (newDrugNumberInput) {
(newDrugNumberInput as HTMLElement).focus();
}
}, 100);
}
/**
* 中药气泡:替换已选行药名
* @param index 行下标
* @param item 商品检索完整行
*/
function onSelectOldChineseDrug(index: number, item: any) {
if (guardSpecialPrescriptionCartEdit()) return;
const drugId = Number(item?.drug?.id || item?.drug_id || 0);
if (!drugId) return;
const check = currentDrugs.value.find(
(v, i) => i !== index && Number(v.id) === drugId,
);
if (check) {
message.error('该药品已经在处方中了!');
return;
}
const data = item;
const newProduct = {
index_id: data.id,
id: data.drug.id,
drug_name: data.drug.drug_name,
number: data.drug.number,
use_num: drugTime.value.find((u) => u.id === data.drug.time_id),
use_type: drugUseType.value.find((u) => u.id === data.drug.type_id),
use_frequency: drugUseFrequency.value.find(
(u) => u.id === data.drug.frequency_id,
),
unit:
data.drug?.unit ||
drugUnit.value.find((u) => u.id === data.drug.unit_id),
price: data.price,
buy_price: data.buy_price,
way_id: data.drug?.way_id,
use_ways: drugUseWay.value.find((u) => u.id === data.drug.way_id),
time_id: data.drug.time_id,
type_id: data.drug.type_id,
frequency_id: data.drug.frequency_id,
unit_id: data.drug.unit_id,
image: data.drug.image,
instruction: data.drug.instruction,
type: data.drug.type,
};
currentDrugs.value[index] = newProduct;
updateLocalStorage();
setTimeout(() => {
const numberInput = document.querySelector(
`.old-number-input-${index} input`,
);
if (numberInput) {
(numberInput as HTMLElement).focus();
}
}, 100);
}
/** 兼容西药等仍走 Select 的旧逻辑 */
function selectNewDrugInfo() {
// 如果是新药品输入框,调用添加新药品方法
const check = currentDrugs.value.find((v) => v.id === newDrugInfo.value.id);
if (check) {
newDrugInfo.value.id = '';
message.error('该药品已经在处方中了!');
return;
}
const data = drugList.value.find((v) => v.drug_id === newDrugInfo.value.id);
if (!data) return;
pendingNewChineseProduct.value = data;
newDrugInfo.value.price = data.price;
newDrugInfo.value.name = data.drug.drug_name;
// 选药后立刻带上真实单位,供新行数量后缀展示(优先接口返回的 unit
newDrugInfo.value.unit =
data.drug?.unit ||
drugUnit.value.find((item) => item.id === data.drug?.unit_id);
newDrugInfo.value.unit_id = data.drug?.unit_id;
setTimeout(() => {
// 获取新药品卡片中的数量输入框并聚焦
const newDrugNumberInput = document.querySelector(
'.new-number-input input',
);
@@ -2634,7 +2722,7 @@ const selectChineseIndex = ref(-1);
const selectChineseId = ref(0);
/**
* 选择药品
* 选择药品(西药等 Select 仍用)
* @param id
*/
function selectOldDrugInfo(id) {
@@ -2648,59 +2736,36 @@ function selectOldDrugInfo(id) {
message.error('该药品已经在处方中了!');
return;
}
const oldDrugInfo = currentDrugs.value[selectChineseIndex.value];
// 如果是新药品输入框,调用添加新药品方法
const data = drugList.value.find((v) => v.drug_id === id);
// 创建新的商品对象(避免直接修改原始数据)
if (!data) return;
const newProduct = {
// 索引ID用于在列表中查找
index_id: data.id,
// 药品ID
id: data.drug.id,
// 药品名称
drug_name: data.drug.drug_name,
// 药品数量
number: data.drug.number,
// 药品使用数量信息
use_num: drugTime.value.find((item) => item.id === data.drug.time_id),
// 药品使用类型信息
use_type: drugUseType.value.find((item) => item.id === data.drug.type_id),
// 药品使用频率信息
use_frequency: drugUseFrequency.value.find(
(item) => item.id === data.drug.frequency_id,
),
// 药品单位:优先用选药接口返回的 unit再回退本地字典
unit:
data.drug?.unit ||
drugUnit.value.find((item) => item.id === data.drug.unit_id),
// 药品价格
price: data.price,
buy_price: data.buy_price,
// 药品使用方式ID
way_id: data.drug?.way_id,
// 药品使用方式信息
use_ways: drugUseWay.value.find((item) => item.id === data.drug.way_id),
// 药品使用时间ID
time_id: data.drug.time_id,
// 药品类型ID
type_id: data.drug.type_id,
// 药品使用频率ID
frequency_id: data.drug.frequency_id,
// 药品单位ID
unit_id: data.drug.unit_id,
// 药品图片
image: data.drug.image,
// 药品说明书
instruction: data.drug.instruction,
// 药品类型
type: data.drug.type,
};
currentDrugs.value[selectChineseIndex.value] = newProduct;
updateLocalStorage();
// const data = drugList.value.find((v) => v.drug_id === id);
setTimeout(() => {
// 获取新药品卡片中的克数输入框并聚焦
const newDrugNumberInput = document.querySelector(
`.old-number-input-${selectChineseIndex.value} input`,
);
@@ -2732,7 +2797,6 @@ function searchOption(inputValue) {
*/
function addDrugByChinese() {
if (guardSpecialPrescriptionCartEdit()) return;
// 如果是新药品输入框,调用添加新药品方法
const check = currentDrugs.value.find(
(v) => v.id === newDrugInfo.value.id,
);
@@ -2740,9 +2804,10 @@ function addDrugByChinese() {
message.error('该药品已经在处方中了!');
return;
}
const data = drugList.value.find(
(v) => v.drug_id === newDrugInfo.value.id,
);
// 优先用气泡选中缓存,兼容西药 Select 仍写在 drugList 的场景
const data =
pendingNewChineseProduct.value ||
drugList.value.find((v) => v.drug_id === newDrugInfo.value.id);
if (data === null || data === undefined) {
message.error('请选择药品');
return;
@@ -2751,6 +2816,7 @@ function addDrugByChinese() {
data.drug.way_id = newDrugInfo.value.way_id;
addProducts(data);
newDrugInfo.value = {};
pendingNewChineseProduct.value = null;
}
/**
* 添加操作
@@ -3430,32 +3496,17 @@ function onStoreSelectOpenChange(open: boolean) {
<div>
<span class="card-index">{{ index + 1 }}</span>
<p>
<Select
v-model:value="drug.id"
<ChineseDrugNameBubble
:display-name="drug.drug_name"
:disabled="isSpecialPrescriptionCartLocked"
:filter-option="false"
class="old-select-drug-name"
style="min-width: 100px"
:store-id="myStoreId"
:register-id="getRegisterId()"
:type="1"
compact
input-class="old-select-drug-name"
placeholder="药名"
show-search
@change="selectOldDrugInfo"
@dropdown-visible-change="
setSelectChineseIndex(index, drug.id)
"
@search="searchOption"
>
<SelectOption v-if="drugList.length === 0" :value="drug.id">
{{ drug.drug_name }}
</SelectOption>
<SelectOption
v-for="(value, index) in drugList"
v-else
:key="index"
:value="value.drug.id"
>
{{ value.drug.drug_name }}
</SelectOption>
</Select>
@select="(item) => onSelectOldChineseDrug(index, item)"
/>
,
<InputNumber
v-model:value="drug.number"
@@ -3523,24 +3574,16 @@ function onStoreSelectOpenChange(open: boolean) {
<div>
<span class="card-index">{{ currentDrugs.length + 1 }}</span>
<div>
<Select
v-model:value="newDrugInfo.id"
:filter-option="false"
class="new-select-drug-name w-1/5"
<ChineseDrugNameBubble
:display-name="newDrugInfo.name || ''"
:store-id="myStoreId"
:register-id="getRegisterId()"
:type="1"
compact
input-class="new-select-drug-name"
placeholder="药名"
show-search
style="min-width: 100px"
@change="selectNewDrugInfo"
@search="searchOption"
>
<SelectOption
v-for="(value, index) in drugList"
:key="index"
:value="value.drug_id"
>
{{ value.drug.drug_name }}
</SelectOption>
</Select>
@select="onSelectNewChineseDrug"
/>
,
<InputNumber
v-model:value="newDrugInfo.number"
@@ -4154,6 +4197,14 @@ function onStoreSelectOpenChange(open: boolean) {
left: 10px;
top: 0;
}
/* 中药卡片:药名气泡锁宽,与克数/用法保持同一行 */
:deep(.chinese-drug-name-bubble) {
width: 100px !important;
min-width: 88px !important;
max-width: 110px !important;
flex: 0 0 100px !important;
vertical-align: middle;
}
.card-price {
position: absolute;

View File

@@ -40,8 +40,8 @@ import {
SelectOption,
Spin,
} from 'ant-design-vue';
import { debounce } from 'lodash-es';
import { ChineseDrugNameBubble } from '#/components/chinese-drug-name-bubble';
import { DrugSearchSelect } from '#/components/drug-search-select';
import {
saveWestCommonPrescriptionApi,
@@ -50,7 +50,6 @@ import {
} from '#/views/doctor/settings/api';
import {
getDrugUseList,
getProductListDoctorReception,
getProcessRuleList,
} from '#/views/doctor/doctor-reception/api';
import {
@@ -173,10 +172,6 @@ const processRuleNoteList = ref<any[]>([]);
*/
let onSaveCallback: (() => void) | null = null;
/**
* 中药搜索关键词列表(每个药品对应一个搜索结果)
*/
const chineseSearchResults = ref<any[]>([]);
/**
* 新药品输入数据
@@ -364,75 +359,38 @@ function handleTypeChange() {
// 切换类型时清空药品列表
drugList.value = [];
newDrugInfo.value = {};
chineseSearchResults.value = [];
}
/**
* 搜索中药(防抖)
* @param keyword 搜索关键词
* 中药气泡:选中新增行
* @param selectedItem 商品检索完整行
*/
const searchChineseDrugs = debounce(async (keyword: string) => {
if (!keyword || keyword.length < 1) {
chineseSearchResults.value = [];
return;
}
try {
const res = await getProductListDoctorReception({
name: keyword,
type: 1, // 中药类型
store_id: userStore.userInfo?.store_id || 2,
});
if (Array.isArray(res)) {
chineseSearchResults.value = res;
} else {
chineseSearchResults.value = [];
}
} catch (error) {
console.error('搜索中药失败:', error);
chineseSearchResults.value = [];
}
}, 300);
/**
* 选择新药品
* @param drugId 药品IDdrug.id
*/
function selectNewDrug(drugId: number) {
// 检查是否已存在
function selectNewDrug(selectedItem: any) {
const drugId = Number(selectedItem?.drug?.id || selectedItem?.drug_id || 0);
if (!drugId) return;
const exists = drugList.value.some(
(item) => item.id === drugId || item.drug_id === drugId,
);
if (exists) {
message.warning('该药品已在列表中');
newDrugInfo.value.id = undefined;
newDrugInfo.value.name = '';
return;
}
// 找到选中的药品信息
const selectedItem = chineseSearchResults.value.find(
(item) => item.drug?.id === drugId || item.drug_id === drugId,
);
if (selectedItem) {
newDrugInfo.value.name = selectedItem.drug?.drug_name || '';
newDrugInfo.value.price = selectedItem.price || 0;
newDrugInfo.value.way_id = 0;
newDrugInfo.value.unit =
selectedItem.drug?.unit || selectedItem.unit || null;
newDrugInfo.value.unit_id =
selectedItem.drug?.unit_id || selectedItem.unit_id || 0;
// 聚焦到数量输入框
nextTick(() => {
const numberInput = document.querySelector('.new-chinese-number input') as HTMLInputElement;
if (numberInput) {
numberInput.focus();
}
});
}
newDrugInfo.value.id = drugId;
newDrugInfo.value.name = selectedItem.drug?.drug_name || '';
newDrugInfo.value.price = selectedItem.price || 0;
newDrugInfo.value.way_id = 0;
newDrugInfo.value.unit =
selectedItem.drug?.unit || selectedItem.unit || null;
newDrugInfo.value.unit_id =
selectedItem.drug?.unit_id || selectedItem.unit_id || 0;
nextTick(() => {
const numberInput = document.querySelector(
'.new-chinese-number input',
) as HTMLInputElement;
numberInput?.focus();
});
}
/**
@@ -474,7 +432,6 @@ function addChineseDrug() {
// 清空并准备下一次输入
newDrugInfo.value = {};
chineseSearchResults.value = [];
message.success('已添加药品');
@@ -488,50 +445,35 @@ function addChineseDrug() {
}
/**
* 修改已有中药(切换药品)
* 中药气泡:替换已有行
* @param index 药品索引
* @param drugId 新药品ID
* @param selectedItem 商品检索完整行
*/
function changeChineseDrug(index: number, drugId: number) {
// 检查是否已存在(排除当前正在修改的)
function changeChineseDrug(index: number, selectedItem: any) {
const drugId = Number(selectedItem?.drug?.id || selectedItem?.drug_id || 0);
if (!drugId) return;
const exists = drugList.value.some(
(item, i) => i !== index && (item.id === drugId || item.drug_id === drugId),
);
if (exists) {
message.warning('该药品已在列表中');
// 恢复原来的值
const drug = drugList.value[index];
if (drug) {
drug.id = drug.drug_id;
}
return;
}
// 找到选中的药品信息
const selectedItem = chineseSearchResults.value.find(
(item) => item.drug?.id === drugId || item.drug_id === drugId,
);
if (selectedItem) {
const drug = drugList.value[index];
if (drug) {
drug.id = drugId;
drug.drug_id = drugId;
drug.drug_name = selectedItem.drug?.drug_name || '';
drug.price = selectedItem.price || 0;
drug.unit = selectedItem.drug?.unit || selectedItem.unit || null;
drug.unit_id = selectedItem.drug?.unit_id || selectedItem.unit_id || 0;
}
// 聚焦到数量输入框
nextTick(() => {
const numberInput = document.querySelector(`.chinese-number-${index} input`) as HTMLInputElement;
if (numberInput) {
numberInput.focus();
}
});
const drug = drugList.value[index];
if (drug) {
drug.id = drugId;
drug.drug_id = drugId;
drug.drug_name = selectedItem.drug?.drug_name || '';
drug.price = selectedItem.price || 0;
drug.unit = selectedItem.drug?.unit || selectedItem.unit || null;
drug.unit_id = selectedItem.drug?.unit_id || selectedItem.unit_id || 0;
}
nextTick(() => {
const numberInput = document.querySelector(
`.chinese-number-${index} input`,
) as HTMLInputElement;
numberInput?.focus();
});
}
/**
@@ -779,7 +721,7 @@ async function handleSave() {
</script>
<template>
<Modal class="w-[900px]">
<Modal class="w-[50%]">
<Spin :spinning="isSaving">
<div class="p-4">
<!-- 类型选择 -->
@@ -1100,30 +1042,15 @@ async function handleSave() {
<div class="chinese-drug-content">
<span class="chinese-drug-index">{{ index + 1 }}</span>
<div class="chinese-drug-form">
<Select
v-model:value="drug.id"
show-search
:filter-option="false"
<ChineseDrugNameBubble
:display-name="drug.drug_name"
:store-id="userStore.userInfo?.store_id || 0"
:type="1"
compact
input-class="chinese-drug-name"
placeholder="药名"
class="chinese-drug-name"
@search="searchChineseDrugs"
@change="(val) => changeChineseDrug(index, val)"
>
<SelectOption
v-if="chineseSearchResults.length === 0"
:value="drug.id"
>
{{ drug.drug_name }}
</SelectOption>
<SelectOption
v-for="item in chineseSearchResults"
v-else
:key="item.drug?.id || item.id"
:value="item.drug?.id || item.id"
>
{{ item.drug?.drug_name || item.drug_name }}
</SelectOption>
</Select>
@select="(item) => changeChineseDrug(index, item)"
/>
<span class="chinese-drug-comma"></span>
<InputNumber
v-model:value="drug.number"
@@ -1169,23 +1096,15 @@ async function handleSave() {
<div class="chinese-drug-content">
<span class="chinese-drug-index">{{ drugList.length + 1 }}</span>
<div class="chinese-drug-form">
<Select
v-model:value="newDrugInfo.id"
show-search
:filter-option="false"
<ChineseDrugNameBubble
:display-name="newDrugInfo.name || ''"
:store-id="userStore.userInfo?.store_id || 0"
:type="1"
compact
input-class="chinese-drug-name new-chinese-name"
placeholder="药名"
class="chinese-drug-name new-chinese-name"
@search="searchChineseDrugs"
@change="selectNewDrug"
>
<SelectOption
v-for="item in chineseSearchResults"
:key="item.drug?.id || item.id"
:value="item.drug?.id || item.id"
>
{{ item.drug?.drug_name || item.drug_name }}
</SelectOption>
</Select>
@select="selectNewDrug"
/>
<span class="chinese-drug-comma"></span>
<InputNumber
v-model:value="newDrugInfo.number"
@@ -1301,15 +1220,18 @@ async function handleSave() {
.chinese-drug-form {
display: flex;
flex-wrap: wrap;
flex-wrap: nowrap;
align-items: center;
gap: 4px;
padding-left: 28px;
overflow: hidden;
}
.chinese-drug-name {
min-width: 80px;
max-width: 120px;
/* 药名气泡锁宽,与克数/用法保持同一行 */
.chinese-drug-form :deep(.chinese-drug-name-bubble) {
width: 100px !important;
min-width: 88px !important;
max-width: 110px !important;
flex: 0 0 100px !important;
}
.chinese-drug-comma {

View File

@@ -39,8 +39,7 @@ import {
Spin,
Tag,
} from 'ant-design-vue';
import { debounce } from 'lodash-es';
import { ChineseDrugNameBubble } from '#/components/chinese-drug-name-bubble';
import { DrugSearchSelect } from '#/components/drug-search-select';
import {
updateWestCommonPrescriptionApi,
@@ -49,7 +48,6 @@ import {
} from '#/views/doctor/settings/api';
import {
getDrugUseList,
getProductListDoctorReception,
getProcessRuleList,
} from '#/views/doctor/doctor-reception/api';
import {
@@ -173,10 +171,6 @@ const processRuleNoteList = ref<any[]>([]);
*/
let onSaveCallback: (() => void) | null = null;
/**
* 中药搜索结果列表
*/
const chineseSearchResults = ref<any[]>([]);
/**
* 新药品输入数据
@@ -287,7 +281,7 @@ const [Modal, modalApi] = useVbenModal({
loadDrugUseData();
// 加载委托调剂选项(第一级:制剂)
await loadProcessRuleData(0, 0);
// 如果有已保存的委托调剂配置,需要级联加载(顺序执行)
if (processRuleId.value) {
await loadProcessRuleData(processRuleId.value, 0);
@@ -410,72 +404,35 @@ function openDiagnosisModal() {
}
/**
* 搜索中药(防抖)
* @param keyword 搜索关键词
* 中药气泡:选中新增行
* @param selectedItem 商品检索完整行
*/
const searchChineseDrugs = debounce(async (keyword: string) => {
if (!keyword || keyword.length < 1) {
chineseSearchResults.value = [];
return;
}
try {
const res = await getProductListDoctorReception({
name: keyword,
type: 1, // 中药类型
store_id: userStore.userInfo?.store_id || 2,
});
if (Array.isArray(res)) {
chineseSearchResults.value = res;
} else {
chineseSearchResults.value = [];
}
} catch (error) {
console.error('搜索中药失败:', error);
chineseSearchResults.value = [];
}
}, 300);
/**
* 选择新药品
* @param drugId 药品ID
*/
function selectNewDrug(drugId: number) {
// 检查是否已存在
function selectNewDrug(selectedItem: any) {
const drugId = Number(selectedItem?.drug?.id || selectedItem?.drug_id || 0);
if (!drugId) return;
const exists = drugList.value.some(
(item) => item.id === drugId || item.drug_id === drugId,
);
if (exists) {
message.warning('该药品已在列表中');
newDrugInfo.value.id = undefined;
newDrugInfo.value.name = '';
return;
}
// 找到选中的药品信息
const selectedItem = chineseSearchResults.value.find(
(item) => item.drug?.id === drugId || item.drug_id === drugId,
);
if (selectedItem) {
newDrugInfo.value.name = selectedItem.drug?.drug_name || '';
newDrugInfo.value.price = selectedItem.price || 0;
newDrugInfo.value.way_id = 0;
// 选药后带上真实单位,供数量后缀展示
newDrugInfo.value.unit =
selectedItem.drug?.unit || selectedItem.unit || null;
newDrugInfo.value.unit_id =
selectedItem.drug?.unit_id || selectedItem.unit_id || 0;
// 聚焦到数量输入框
nextTick(() => {
const numberInput = document.querySelector('.edit-new-chinese-number input') as HTMLInputElement;
if (numberInput) {
numberInput.focus();
}
});
}
newDrugInfo.value.id = drugId;
newDrugInfo.value.name = selectedItem.drug?.drug_name || '';
newDrugInfo.value.price = selectedItem.price || 0;
newDrugInfo.value.way_id = 0;
newDrugInfo.value.unit =
selectedItem.drug?.unit || selectedItem.unit || null;
newDrugInfo.value.unit_id =
selectedItem.drug?.unit_id || selectedItem.unit_id || 0;
nextTick(() => {
const numberInput = document.querySelector(
'.edit-new-chinese-number input',
) as HTMLInputElement;
numberInput?.focus();
});
}
/**
@@ -517,7 +474,6 @@ function addChineseDrug() {
// 清空并准备下一次输入
newDrugInfo.value = {};
chineseSearchResults.value = [];
message.success('已添加药品');
@@ -531,50 +487,35 @@ function addChineseDrug() {
}
/**
* 修改已有中药(切换药品)
* 中药气泡:替换已有行
* @param index 药品索引
* @param drugId 新药品ID
* @param selectedItem 商品检索完整行
*/
function changeChineseDrug(index: number, drugId: number) {
// 检查是否已存在(排除当前正在修改的)
function changeChineseDrug(index: number, selectedItem: any) {
const drugId = Number(selectedItem?.drug?.id || selectedItem?.drug_id || 0);
if (!drugId) return;
const exists = drugList.value.some(
(item, i) => i !== index && (item.id === drugId || item.drug_id === drugId),
);
if (exists) {
message.warning('该药品已在列表中');
// 恢复原来的值
const drug = drugList.value[index];
if (drug) {
drug.id = drug.drug_id;
}
return;
}
// 找到选中的药品信息
const selectedItem = chineseSearchResults.value.find(
(item) => item.drug?.id === drugId || item.drug_id === drugId,
);
if (selectedItem) {
const drug = drugList.value[index];
if (drug) {
drug.id = drugId;
drug.drug_id = drugId;
drug.drug_name = selectedItem.drug?.drug_name || '';
drug.price = selectedItem.price || 0;
drug.unit = selectedItem.drug?.unit || selectedItem.unit || null;
drug.unit_id = selectedItem.drug?.unit_id || selectedItem.unit_id || 0;
}
// 聚焦到数量输入框
nextTick(() => {
const numberInput = document.querySelector(`.edit-chinese-number-${index} input`) as HTMLInputElement;
if (numberInput) {
numberInput.focus();
}
});
const drug = drugList.value[index];
if (drug) {
drug.id = drugId;
drug.drug_id = drugId;
drug.drug_name = selectedItem.drug?.drug_name || '';
drug.price = selectedItem.price || 0;
drug.unit = selectedItem.drug?.unit || selectedItem.unit || null;
drug.unit_id = selectedItem.drug?.unit_id || selectedItem.unit_id || 0;
}
nextTick(() => {
const numberInput = document.querySelector(
`.edit-chinese-number-${index} input`,
) as HTMLInputElement;
numberInput?.focus();
});
}
/**
@@ -807,7 +748,7 @@ async function handleSave() {
</script>
<template>
<Modal class="w-[900px]">
<Modal class="w-[50%]">
<Spin :spinning="isSaving">
<div class="p-4">
<!-- 类型标签 -->
@@ -1119,30 +1060,15 @@ async function handleSave() {
<div class="chinese-drug-content">
<span class="chinese-drug-index">{{ index + 1 }}</span>
<div class="chinese-drug-form">
<Select
v-model:value="drug.id"
show-search
:filter-option="false"
<ChineseDrugNameBubble
:display-name="drug.drug_name"
:store-id="userStore.userInfo?.store_id || 0"
:type="1"
compact
input-class="chinese-drug-name"
placeholder="药名"
class="chinese-drug-name"
@search="searchChineseDrugs"
@change="(val) => changeChineseDrug(index, val)"
>
<SelectOption
v-if="chineseSearchResults.length === 0"
:value="drug.id"
>
{{ drug.drug_name }}
</SelectOption>
<SelectOption
v-for="item in chineseSearchResults"
v-else
:key="item.drug?.id || item.id"
:value="item.drug?.id || item.id"
>
{{ item.drug?.drug_name || item.drug_name }}
</SelectOption>
</Select>
@select="(item) => changeChineseDrug(index, item)"
/>
<span class="chinese-drug-comma"></span>
<InputNumber
v-model:value="drug.number"
@@ -1188,23 +1114,15 @@ async function handleSave() {
<div class="chinese-drug-content">
<span class="chinese-drug-index">{{ drugList.length + 1 }}</span>
<div class="chinese-drug-form">
<Select
v-model:value="newDrugInfo.id"
show-search
:filter-option="false"
<ChineseDrugNameBubble
:display-name="newDrugInfo.name || ''"
:store-id="userStore.userInfo?.store_id || 0"
:type="1"
compact
input-class="chinese-drug-name edit-new-chinese-name"
placeholder="药名"
class="chinese-drug-name edit-new-chinese-name"
@search="searchChineseDrugs"
@change="selectNewDrug"
>
<SelectOption
v-for="item in chineseSearchResults"
:key="item.drug?.id || item.id"
:value="item.drug?.id || item.id"
>
{{ item.drug?.drug_name || item.drug_name }}
</SelectOption>
</Select>
@select="selectNewDrug"
/>
<span class="chinese-drug-comma"></span>
<InputNumber
v-model:value="newDrugInfo.number"
@@ -1290,15 +1208,18 @@ async function handleSave() {
.chinese-drug-form {
display: flex;
flex-wrap: wrap;
flex-wrap: nowrap;
align-items: center;
gap: 4px;
padding-left: 28px;
overflow: hidden;
}
.chinese-drug-name {
min-width: 80px;
max-width: 120px;
/* 药名气泡锁宽,与克数/用法保持同一行 */
.chinese-drug-form :deep(.chinese-drug-name-bubble) {
width: 100px !important;
min-width: 88px !important;
max-width: 110px !important;
flex: 0 0 100px !important;
}
.chinese-drug-comma {

View File

@@ -110,12 +110,15 @@ function openStoreCard(row: Record<string, any>) {
StoreCardModalApi.open();
}
// 打开二维码预览弹窗
const openQrCodeModal = (url, title, address) => {
/**
* 打开药店二维码预览(本页固定 type=1
*/
const openQrCodeModal = (url, title, address, type = 1) => {
QrCodePreviewApi.setData({
values: url, // 二维码图片URL
title, // 标题
address, // 地址
values: url,
title,
address,
type,
});
QrCodePreviewApi.open();
};
@@ -534,6 +537,7 @@ const batchSyncDrugPrice = () => {
row.qr_code,
row.name,
`${row.province.name}${row.city.name}${row.position}`,
row.type ?? 1,
)
"
/>

View File

@@ -149,12 +149,15 @@ const openAddDoctorModal = (row: any) => {
addDoctorModalApi.open();
};
const openQrCodeModal = (url, title, address) => {
/**
* 打开诊所二维码预览(本页固定 type=0
*/
const openQrCodeModal = (url, title, address, type = 0) => {
QrCodePreviewApi.setData({
// 表单值
values: url,
title,
address,
type,
});
QrCodePreviewApi.open();
};
@@ -618,6 +621,7 @@ const handleSwitchClinicType = (row: any) => {
row.qr_code,
row.name,
`${row.province.name}${row.city.name}${row.position}`,
row.type ?? 0,
)
"
/>