feat: 药品别名

This commit is contained in:
李琦
2026-08-07 14:25:19 +08:00
parent 5b9f3ec3ed
commit efd7e8ffc8
29 changed files with 1158 additions and 58 deletions

View File

@@ -2,7 +2,8 @@
/**
* 中药药名气泡选择器
* - 对齐诊断/医嘱 EntryKeywordBubbleInput 聚焦弹出 Teleport 浮层
* - 远程搜索药品getProductListDoctorReception拼音/名称高亮
* - 远程搜索药品getProductListDoctorReception支持药名/别名/首拼/全拼/数字
* - 列表展示拼音列;命中别名时展示匹配别名及其拼音
* - 选中后回传完整商品行,供接诊/复诊/特色方/常用方复用现有落库逻辑
*/
import {
@@ -65,11 +66,11 @@ const emit = defineEmits<{
select: [item: ChineseDrugBubbleItem];
}>();
const ROW_HEIGHT = 44;
const ROW_HEIGHT = 48;
const HINT_HEIGHT = 34;
const HEAD_HEIGHT = 32;
const PANEL_PAD = 8;
const PREFERRED_PANEL = 320;
const PREFERRED_PANEL = 360;
const containerRef = ref<HTMLElement | null>(null);
const panelRef = ref<HTMLElement | null>(null);
@@ -127,6 +128,31 @@ function drugNameOf(item: ChineseDrugBubbleItem): string {
return String(item?.drug?.drug_name || item?.drug_name || '').trim();
}
/** 关键词命中的别名(后端 matched_alias */
function matchedAliasOf(item: ChineseDrugBubbleItem): string {
return String(
item?.matched_alias || item?.drug?.matched_alias || '',
).trim();
}
/**
* 拼音展示:命中别名时用别名拼音,否则用药名首拼(全拼)
*/
function pinyinLabelOf(item: ChineseDrugBubbleItem): string {
const aliasPy = String(
item?.matched_alias_pinyin || item?.drug?.matched_alias_pinyin || '',
).trim();
if (matchedAliasOf(item) && aliasPy) return aliasPy;
const display = String(item?.drug?.pinyin_display || '').trim();
if (display) return display;
const simple = String(item?.drug?.pinyin_simple || '').trim();
const full = String(item?.drug?.pinyin_full || '').trim();
if (simple && full && simple.toLowerCase() !== full.toLowerCase()) {
return `${simple}(${full})`;
}
return simple || full || '-';
}
function unitLabelOf(item: ChineseDrugBubbleItem): string {
const unit = item?.drug?.unit || item?.unit;
if (typeof unit === 'object' && unit?.name) return String(unit.name);
@@ -155,7 +181,8 @@ function updatePanelPlacement() {
const panelMax = Math.min(PREFERRED_PANEL, avail);
listViewportH.value = Math.max(80, panelMax - HINT_HEIGHT - HEAD_HEIGHT);
const PANEL_MIN_WIDTH = 420;
// 含拼音列后加宽,避免别名/全拼被裁切
const PANEL_MIN_WIDTH = 560;
const width = Math.min(
Math.max(rect.width, PANEL_MIN_WIDTH),
vw - PANEL_PAD * 2,
@@ -228,9 +255,13 @@ async function fetchDrugs(kw: string) {
(it) => drugNameOf(it),
(it) =>
[
it.drug?.pinyin,
it.drug?.pinyin_simple,
it.drug?.pinyin_full,
it.drug?.initials_of_pinyin,
it.drug?.pinyin_display,
it.drug?.drug_alias,
it.matched_alias || it.drug?.matched_alias,
it.matched_alias_pinyin || it.drug?.matched_alias_pinyin,
it.drug?.search_digits,
it.drug?.specification,
]
.map((v) => String(v || ''))
@@ -440,7 +471,9 @@ defineExpose({
v-if="!loading && options.length === 0"
:image="Empty.PRESENTED_IMAGE_SIMPLE"
:description="
keyword.trim() ? '未找到匹配药材' : '请输入药名/拼音搜索'
keyword.trim()
? '未找到匹配药材'
: '请输入药名/别名/拼音/数字搜索'
"
class="py-4"
/>
@@ -450,6 +483,7 @@ defineExpose({
:class="{ 'has-score': !!keyword.trim() }"
>
<span class="col-name">药名</span>
<span class="col-py">拼音</span>
<span class="col-spec">规格/单位</span>
<span class="col-price">价格</span>
<span v-if="keyword.trim()" class="col-score">匹配度</span>
@@ -480,13 +514,47 @@ defineExpose({
@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}`"
<span
class="col-name"
:title="
matchedAliasOf(item)
? `${drugNameOf(item)}(别名:${matchedAliasOf(item)}`
: drugNameOf(item)
"
>
<span class="col-name__main">
<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
v-if="matchedAliasOf(item)"
class="col-name__alias"
>
<em v-if="p.hit" class="hit">{{ p.text }}</em>
<template v-else>{{ p.text }}</template>
别名
<template
v-for="(p, pi) in partsOf(matchedAliasOf(item))"
:key="`a_${pi}`"
>
<em v-if="p.hit" class="hit">{{ p.text }}</em>
<template v-else>{{ p.text }}</template>
</template>
</span>
</span>
<span class="col-py" :title="pinyinLabelOf(item)">
<template v-if="pinyinLabelOf(item) === '-'">-</template>
<template v-else>
<template
v-for="(p, pi) in partsOf(pinyinLabelOf(item))"
:key="`py_${pi}`"
>
<em v-if="p.hit" class="hit">{{ p.text }}</em>
<template v-else>{{ p.text }}</template>
</template>
</template>
</span>
<span class="col-spec" :title="unitLabelOf(item)">
@@ -589,7 +657,7 @@ defineExpose({
.chinese-drug-name-bubble__head,
.chinese-drug-name-bubble__row {
display: grid;
grid-template-columns: 1fr 100px 72px;
grid-template-columns: minmax(90px, 1.1fr) minmax(110px, 1.3fr) 90px 64px;
gap: 8px;
padding: 6px 12px;
font-size: 13px;
@@ -598,7 +666,7 @@ defineExpose({
}
.chinese-drug-name-bubble__head.has-score,
.chinese-drug-name-bubble__row.has-score {
grid-template-columns: 1fr 90px 64px 48px;
grid-template-columns: minmax(90px, 1.1fr) minmax(100px, 1.2fr) 80px 56px 48px;
}
.chinese-drug-name-bubble__head {
flex-shrink: 0;
@@ -617,13 +685,37 @@ defineExpose({
color: hsl(var(--accent-foreground));
}
.col-name,
.col-py,
.col-spec {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.col-name {
display: flex;
flex-direction: column;
gap: 1px;
min-width: 0;
font-weight: 500;
white-space: normal;
line-height: 1.25;
}
.col-name__main {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.col-name__alias {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 11px;
font-weight: 400;
color: hsl(var(--muted-foreground));
}
.col-py {
font-size: 12px;
color: hsl(var(--muted-foreground));
}
.col-spec {
font-size: 12px;
@@ -641,7 +733,9 @@ defineExpose({
color: #d4380d;
white-space: nowrap;
}
.chinese-drug-name-bubble__row.is-active .col-spec {
.chinese-drug-name-bubble__row.is-active .col-spec,
.chinese-drug-name-bubble__row.is-active .col-py,
.chinese-drug-name-bubble__row.is-active .col-name__alias {
color: inherit;
opacity: 0.85;
}

View File

@@ -43,6 +43,10 @@ export interface WarehouseDrugItem {
id: number;
drug_name: string;
pinyin_simple?: string;
/** 关键词命中的别名(后端 matched_alias */
matched_alias?: string;
/** 命中别名对应拼音展示 */
matched_alias_pinyin?: string;
specification?: string;
image?: string;
type?: number;
@@ -212,8 +216,27 @@ watch(
<div v-else class="warehouse-drug-item__img-placeholder">无图</div>
</div>
<div class="warehouse-drug-item__info">
<div class="warehouse-drug-item__name">{{ item.drug_name }}</div>
<div
class="warehouse-drug-item__name"
:title="
item.matched_alias
? `${item.drug_name}(别名:${item.matched_alias}`
: item.drug_name
"
>
{{ item.drug_name }}
</div>
<!-- 与开方气泡一致命中别名时展示别名及拼音 -->
<div v-if="item.matched_alias" class="warehouse-drug-item__alias">
别名{{ item.matched_alias }}
<span v-if="item.matched_alias_pinyin">
{{ item.matched_alias_pinyin }}
</span>
</div>
<div class="warehouse-drug-item__meta">
<span v-if="item.pinyin_simple && !item.matched_alias" class="warehouse-drug-item__py">
拼音{{ item.pinyin_simple }}
</span>
<span v-if="item.specification" class="warehouse-drug-item__spec">
规格{{ item.specification }}
</span>
@@ -321,7 +344,17 @@ watch(
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 2px;
}
&__alias {
font-size: 12px;
color: #8c8c8c;
line-height: 1.3;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__meta {
@@ -332,6 +365,10 @@ watch(
color: #999;
}
&__py {
color: #8c8c8c;
}
&__spec {
color: #666;
}