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

View File

@@ -0,0 +1,45 @@
import { requestClient } from '#/api/request';
const prefix = 'drug-alias/';
/**
* 药品详情 + 别名(药名点击弹窗)
*/
export async function getDrugAliasDetail(drugId: number) {
return requestClient.get<any>(`${prefix}detail`, {
params: { drug_id: drugId },
});
}
/**
* 整表覆盖保存别名(管理员/超管)
*/
export async function saveDrugAliases(
drugId: number,
aliases: string | string[],
) {
return requestClient.post<any>(`${prefix}save-aliases`, {
drug_id: drugId,
aliases,
});
}
/**
* 新增一条别名
*/
export async function addDrugAlias(drugId: number, alias: string) {
return requestClient.post<any>(`${prefix}add-alias`, {
drug_id: drugId,
alias,
});
}
/**
* 删除一条别名
*/
export async function deleteDrugAlias(drugId: number, aliasId: number) {
return requestClient.post<any>(`${prefix}delete-alias`, {
drug_id: drugId,
alias_id: aliasId,
});
}

View File

@@ -0,0 +1,354 @@
<script lang="ts" setup>
/**
* 药品详情弹窗:展示基础信息;管理员/超管可逐条添加/删除别名
*/
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import {
Button,
Descriptions,
DescriptionsItem,
Empty,
Image,
Input,
Popconfirm,
Spin,
Tag,
message,
} from 'ant-design-vue';
import {
addDrugAlias,
deleteDrugAlias,
getDrugAliasDetail,
} from './drug-alias-api';
type AliasRow = {
id?: number;
alias: string;
pinyin_simple?: string;
pinyin_full?: string;
pinyin_display?: string;
search_digits?: string;
};
const loading = ref(false);
const adding = ref(false);
const deletingId = ref(0);
const drug = ref<Record<string, any>>({});
const aliases = ref<AliasRow[]>([]);
const canManage = ref(false);
/** 新增输入框 */
const newAlias = ref('');
const gridApiRef = ref<any>(null);
const titleText = computed(
() => `药品详情${drug.value?.drug_name ? ` · ${drug.value.drug_name}` : ''}`,
);
const aliasCountText = computed(() => `${aliases.value.length}`);
/**
* 用接口结果回填本地状态
*/
function applyDetail(res: any) {
drug.value = res?.drug || {};
aliases.value = Array.isArray(res?.aliases) ? res.aliases : [];
canManage.value = !!res?.can_manage;
}
/**
* 加载详情
*/
async function loadDetail(drugId: number) {
loading.value = true;
newAlias.value = '';
try {
const res = await getDrugAliasDetail(drugId);
applyDetail(res);
} catch (e) {
console.error(e);
drug.value = {};
aliases.value = [];
canManage.value = false;
} finally {
loading.value = false;
}
}
const [Modal, modalApi] = useVbenModal({
class: 'w-[720px]',
draggable: true,
fullscreenButton: false,
showConfirmButton: false,
cancelText: '关闭',
onOpenChange(isOpen: boolean) {
if (!isOpen) {
newAlias.value = '';
return;
}
const data = modalApi.getData<{
drug_id?: number;
id?: number;
gridApi?: any;
}>();
gridApiRef.value = data?.gridApi || null;
const id = Number(data?.drug_id || data?.id || 0);
if (id > 0) {
void loadDetail(id);
}
},
});
/**
* 添加一条别名(支持一次输入多个,按分隔符拆)
*/
async function handleAddAlias() {
const id = Number(drug.value?.id || 0);
if (!id || !canManage.value) return;
const parts = String(newAlias.value || '')
.split(/[\n,,、;|]+/)
.map((s) => s.trim())
.filter(Boolean);
if (parts.length === 0) {
message.warning('请输入别名');
return;
}
adding.value = true;
try {
let lastRes: any = null;
for (const part of parts) {
lastRes = await addDrugAlias(id, part);
}
if (lastRes) applyDetail(lastRes);
newAlias.value = '';
message.success(parts.length > 1 ? `已添加 ${parts.length} 条别名` : '已添加');
gridApiRef.value?.query?.();
} finally {
adding.value = false;
}
}
/**
* 删除一条别名
*/
async function handleDeleteAlias(row: AliasRow) {
const drugId = Number(drug.value?.id || 0);
const aliasId = Number(row?.id || 0);
if (!drugId || !aliasId || !canManage.value) return;
deletingId.value = aliasId;
try {
const res = await deleteDrugAlias(drugId, aliasId);
applyDetail(res);
message.success('已删除');
gridApiRef.value?.query?.();
} finally {
deletingId.value = 0;
}
}
</script>
<template>
<Modal :title="titleText">
<Spin :spinning="loading">
<div v-if="drug?.id" class="drug-detail">
<div class="drug-detail__head">
<Image
v-if="drug.image"
:src="drug.image"
:width="64"
:height="64"
class="drug-detail__img"
/>
<div v-else class="drug-detail__img drug-detail__img--empty">无图</div>
<div class="drug-detail__head-text">
<div class="drug-detail__name">{{ drug.drug_name || '-' }}</div>
<div class="drug-detail__meta">
ID{{ drug.id }}
<span v-if="drug.drug_number"> · 编号{{ drug.drug_number }}</span>
</div>
</div>
</div>
<Descriptions bordered size="small" :column="2" class="mt-3">
<DescriptionsItem label="拼音首拼">
{{ drug.pinyin_simple || '-' }}
</DescriptionsItem>
<DescriptionsItem label="拼音全拼">
{{ drug.pinyin_full || '-' }}
</DescriptionsItem>
<DescriptionsItem label="检索数字">
{{ drug.search_digits || '-' }}
</DescriptionsItem>
<DescriptionsItem label="规格">
{{ drug.specification || '-' }}
</DescriptionsItem>
<DescriptionsItem label="单位">
{{ drug.unit_name || '-' }}
</DescriptionsItem>
<DescriptionsItem label="供应商">
{{ drug.supplier_name || '-' }}
</DescriptionsItem>
</Descriptions>
<div class="drug-detail__alias-head">
<div class="drug-detail__alias-title">
别名
<Tag v-if="canManage" color="processing" class="ml-1">可维护</Tag>
<Tag v-else class="ml-1">只读</Tag>
<span class="drug-detail__alias-count">{{ aliasCountText }}</span>
</div>
</div>
<!-- 管理员顶部添加区 -->
<div v-if="canManage" class="drug-detail__alias-add">
<Input
v-model:value="newAlias"
allow-clear
placeholder="输入别名后回车添加;多个可用顿号/逗号分隔"
:disabled="adding"
@press-enter="handleAddAlias"
/>
<Button
type="primary"
class="ml-2"
:loading="adding"
@click="handleAddAlias"
>
添加
</Button>
</div>
<div class="drug-detail__alias-list">
<Empty
v-if="aliases.length === 0"
:image="Empty.PRESENTED_IMAGE_SIMPLE"
description="暂无别名"
/>
<div
v-for="(a, idx) in aliases"
:key="`${a.id || idx}_${a.alias}`"
class="drug-detail__alias-row"
>
<div class="drug-detail__alias-main">
<span class="drug-detail__alias-name">{{ a.alias }}</span>
<span class="drug-detail__alias-py">
{{ a.pinyin_display || a.pinyin_simple || '-' }}
</span>
</div>
<Popconfirm
v-if="canManage"
title="确认删除该别名?"
ok-text="删除"
cancel-text="取消"
@confirm="handleDeleteAlias(a)"
>
<Button
type="link"
danger
size="small"
:loading="deletingId === Number(a.id || 0)"
>
删除
</Button>
</Popconfirm>
</div>
</div>
</div>
<Empty v-else-if="!loading" description="未找到药品" />
</Spin>
</Modal>
</template>
<style scoped>
.drug-detail__head {
display: flex;
gap: 12px;
align-items: center;
}
.drug-detail__img {
border-radius: 6px;
object-fit: cover;
flex-shrink: 0;
}
.drug-detail__img--empty {
width: 64px;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: hsl(var(--muted-foreground));
background: hsl(var(--muted) / 0.3);
border: 1px solid hsl(var(--border));
}
.drug-detail__name {
font-size: 16px;
font-weight: 600;
line-height: 1.4;
}
.drug-detail__meta {
margin-top: 4px;
font-size: 12px;
color: hsl(var(--muted-foreground));
}
.drug-detail__alias-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 16px;
margin-bottom: 8px;
}
.drug-detail__alias-title {
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 4px;
}
.drug-detail__alias-count {
margin-left: 6px;
font-size: 12px;
font-weight: 400;
color: hsl(var(--muted-foreground));
}
.drug-detail__alias-add {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.drug-detail__alias-list {
border: 1px solid hsl(var(--border));
border-radius: 6px;
min-height: 80px;
max-height: 320px;
overflow-y: auto;
}
.drug-detail__alias-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 8px 10px;
border-bottom: 1px solid hsl(var(--border) / 0.7);
font-size: 13px;
}
.drug-detail__alias-row:last-child {
border-bottom: none;
}
.drug-detail__alias-main {
min-width: 0;
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
}
.drug-detail__alias-name {
font-weight: 500;
word-break: break-all;
}
.drug-detail__alias-py {
color: hsl(var(--muted-foreground));
font-size: 12px;
}
</style>

View File

@@ -1,14 +1,24 @@
<script lang="ts" setup>
/**
* 商品信息合并列:图片 + 名称/ID/拼音/发布时间 + 说明书预览链接
* 五类商品列表复用,避免每页各写一套 slot
* 点击药名打开详情弹窗(由父级监听 name-click
* 搜索命中别名时展示匹配别名(对齐开方/仓库列表)
*/
import { computed, ref } from 'vue';
import { Image } from 'ant-design-vue';
const props = defineProps<{
row: Record<string, any>;
const props = withDefaults(
defineProps<{
row: Record<string, any>;
/** 药名是否可点击打开详情 */
nameClickable?: boolean;
}>(),
{ nameClickable: true },
);
const emit = defineEmits<{
'name-click': [row: Record<string, any>];
}>();
const previewVisible = ref(false);
@@ -18,10 +28,27 @@ const instructionUrl = computed(() => {
return typeof url === 'string' && url.trim() ? url.trim() : '';
});
/** 当前搜索命中的别名 */
const matchedAlias = computed(() =>
String(props.row?.matched_alias || '').trim(),
);
/** 拼音:命中别名时用别名拼音,否则用药名首拼 */
const pinyinDisplay = computed(() => {
const aliasPy = String(props.row?.matched_alias_pinyin || '').trim();
if (matchedAlias.value && aliasPy) return aliasPy;
return String(props.row?.pinyin_simple || '').trim();
});
function openInstruction() {
if (!instructionUrl.value) return;
previewVisible.value = true;
}
function onNameClick() {
if (!props.nameClickable) return;
emit('name-click', props.row);
}
</script>
<template>
<div class="product-info-cell">
@@ -36,10 +63,25 @@ function openInstruction() {
无图
</div>
<div class="product-info-cell__body">
<div class="product-info-cell__name">{{ row.drug_name || '-' }}</div>
<div
class="product-info-cell__name"
:class="{ 'product-info-cell__name--link': nameClickable }"
:title="
matchedAlias
? `${row.drug_name || '-'}(别名:${matchedAlias}`
: row.drug_name || '-'
"
@click.stop="onNameClick"
>
{{ row.drug_name || '-' }}
</div>
<div v-if="matchedAlias" class="product-info-cell__alias">
别名{{ matchedAlias }}
<span v-if="pinyinDisplay">{{ pinyinDisplay }}</span>
</div>
<div class="product-info-cell__meta">
<span>ID{{ row.id }}</span>
<span v-if="row.pinyin_simple">拼音{{ row.pinyin_simple }}</span>
<span v-if="pinyinDisplay && !matchedAlias">拼音{{ pinyinDisplay }}</span>
<span v-if="row.created_at">发布时间{{ row.created_at }}</span>
</div>
<a
@@ -49,7 +91,6 @@ function openInstruction() {
>
说明书
</a>
<!-- 隐藏 Image 仅用于说明书预览 -->
<Image
v-if="instructionUrl"
:src="instructionUrl"
@@ -96,6 +137,20 @@ function openInstruction() {
line-height: 1.4;
word-break: break-all;
}
.product-info-cell__name--link {
color: hsl(var(--primary));
cursor: pointer;
}
.product-info-cell__name--link:hover {
text-decoration: underline;
}
.product-info-cell__alias {
margin-top: 2px;
font-size: 12px;
color: hsl(var(--muted-foreground));
line-height: 1.3;
word-break: break-all;
}
.product-info-cell__meta {
display: flex;
flex-wrap: wrap;

View File

@@ -96,3 +96,10 @@ export async function applyBatchDrugNumberApi(data: {
}) {
return requestClient.post<any>(`${prefix}apply-batch-drug-number`, data);
}
/**
* 同步空拼音(首拼/全拼/数字串)
*/
export async function syncChinaMedicinePinyin() {
return requestClient.post<any>(`${prefix}sync-pinyin`, {});
}

View File

@@ -53,6 +53,33 @@ export const modalFormProps: VbenFormProps = {
label: '药品别名',
rules: 'required',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '可留空,保存时按药名自动生成',
},
fieldName: 'pinyin_simple',
formItemClass: 'col-span-6',
label: '拼音首拼',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '可留空,保存时按药名自动生成',
},
fieldName: 'pinyin_full',
formItemClass: 'col-span-6',
label: '拼音全拼',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '可留空,保存时从药名提取数字',
},
fieldName: 'search_digits',
formItemClass: 'col-span-6',
label: '检索数字',
},
{
component: 'VbenInput',
componentProps: {

View File

@@ -24,9 +24,11 @@ export const gridOptions: VxeGridProps<RowType> = {
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'id', align: 'left', title: 'ID', width: 100 },
{ field: 'drug_name', align: 'left', title: '药品名称' },
{ field: 'drug_name', align: 'left', title: '药品名称', slots: { default: 'drug_name' } },
{ field: 'drug_number', align: 'left', title: '药品编码erp' },
{ field: 'pinyin_simple', title: '拼音' },
{ field: 'pinyin_simple', title: '首拼', minWidth: 100, slots: { default: 'pinyin_simple' } },
{ field: 'pinyin_full', title: '全拼', minWidth: 140 },
{ field: 'search_digits', title: '数字', width: 90 },
{ field: 'status', title: '状态', slots: { default: 'status' } },
{ field: 'created_at', title: '发布时间' },
{ type: 'html', title: '操作', slots: { default: 'action' } },

View File

@@ -5,20 +5,22 @@ import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Button, Image, message, Tag } from 'ant-design-vue';
import { Button, Image, Modal as AntModal, message, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import { downloadByData } from '#/util/tool';
import { deleteChinaMedicine, exportChinaMedicineApi } from './api';
import { deleteChinaMedicine, exportChinaMedicineApi, syncChinaMedicinePinyin } from './api';
import DrugNumberBatchModal from './components/DrugNumberBatchModal.vue';
import FormModalDemo from './components/modal.vue';
import ExcelUpload from './components/ExcelUpload.vue';
import DrugDetailModal from '#/views/business/product/_shared/drug-detail-modal.vue';
import { formOptions } from './config/search';
import { gridOptions } from './config/table';
const hasTopTableDropDownActions = ref(false);
const syncing = ref(false);
const gridEvents: VxeGridListeners<any> = {
checkboxChange() {
@@ -51,6 +53,10 @@ const [DrugNumberBatchModalComp, drugNumberBatchModalApi] = useVbenModal({
connectedComponent: DrugNumberBatchModal,
});
const [DrugDetailModalComp, drugDetailModalApi] = useVbenModal({
connectedComponent: DrugDetailModal,
});
const showModal = (data = {}, isUpdate = false) => {
formModalApi.setData({
// 表单值
@@ -61,6 +67,36 @@ const showModal = (data = {}, isUpdate = false) => {
formModalApi.open();
};
/** 点击药名:打开详情 / 别名维护弹窗 */
function openDrugDetail(row: any) {
drugDetailModalApi.setData({
drug_id: row?.id,
gridApi,
});
drugDetailModalApi.open();
}
/** 同步本类型空拼音(首拼/全拼/数字) */
function handleSyncPinyin() {
AntModal.confirm({
title: '同步拼音',
content:
'将扫描本类型药品中首拼/全拼/数字串为空的记录并自动生成,已有值不会覆盖。确认执行?',
onOk: async () => {
syncing.value = true;
try {
const res = await syncChinaMedicinePinyin();
message.success(
`同步完成:更新 ${Number(res?.updated || 0)} 条,跳过 ${Number(res?.skipped || 0)}`,
);
gridApi.query();
} finally {
syncing.value = false;
}
},
});
}
const deleteApi = (row: any) => {
let ids = [];
if (row) {
@@ -104,6 +140,7 @@ const openDrugNumberBatchModal = () => {
<Page auto-content-height title="中药管理">
<ExcelUploadModal />
<DrugNumberBatchModalComp />
<DrugDetailModalComp />
<FormModal />
<Grid>
<template #toolbar-buttons>
@@ -157,6 +194,30 @@ const openDrugNumberBatchModal = () => {
</Button>
</template>
</TableAction>
<Button class="ml-2" :loading="syncing" @click="handleSyncPinyin">
同步拼音
</Button>
</template>
<template #drug_name="{ row }">
<div class="cm-drug-name-cell">
<a class="text-primary cursor-pointer" @click="openDrugDetail(row)">
{{ row.drug_name || '-' }}
</a>
<!-- 搜索命中别名时展示对齐开方/仓库 -->
<div v-if="row.matched_alias" class="cm-drug-name-cell__alias">
别名{{ row.matched_alias }}
<span v-if="row.matched_alias_pinyin">
{{ row.matched_alias_pinyin }}
</span>
</div>
</div>
</template>
<template #pinyin_simple="{ row }">
<span>
{{
row.matched_alias_pinyin || row.pinyin_simple || '-'
}}
</span>
</template>
<template #status="{ row }">
<Tag :color="row.status_color">{{ row.status_txt }}</Tag>
@@ -191,3 +252,12 @@ const openDrugNumberBatchModal = () => {
</Grid>
</Page>
</template>
<style scoped>
.cm-drug-name-cell__alias {
margin-top: 2px;
font-size: 12px;
color: hsl(var(--muted-foreground));
line-height: 1.3;
word-break: break-all;
}
</style>

View File

@@ -85,4 +85,11 @@ export async function importHealthFoodApi(data: Record<string, any>) {
*/
export async function updateZoneCategory(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}update-zone-category`, data);
}
/**
* 同步空拼音(首拼/全拼/数字串)
*/
export async function syncHealthFoodPinyin() {
return requestClient.post<any>(`${prefix}sync-pinyin`, {});
}

View File

@@ -9,7 +9,7 @@ import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
// 导入Ant Design Vue组件
import { Button, message, Tag } from 'ant-design-vue';
import { Button, Modal as AntModal, message, Tag } from 'ant-design-vue';
// 导入Vben VxeGrid适配器
import { useVbenVxeGrid } from '#/adapter/vxe-table';
@@ -20,6 +20,7 @@ import { downloadByData } from '#/util/tool';
import CategoryWarehouseCell from '#/views/business/product/_shared/category-warehouse-cell.vue';
import CentralPriceCell from '#/views/business/product/_shared/central-price-cell.vue';
import CentralStockInModal from '#/views/business/product/_shared/central-stock-in-modal.vue';
import DrugDetailModal from '#/views/business/product/_shared/drug-detail-modal.vue';
import DrugFreeShippingCell from '#/views/business/product/_shared/drug-free-shipping-cell.vue';
import ProductInfoCell from '#/views/business/product/_shared/product-info-cell.vue';
import WarehouseDrugPriceModal from '#/views/business/warehouse-drug-management/admin/components/modal.vue';
@@ -27,7 +28,11 @@ import BindListModal from '#/views/system/delivery-warehouse-drug/components/bin
import { resolveCentralPrice } from '#/views/system/delivery-warehouse-drug/utils/resolve-central-price';
// 导入保健食品管理相关API
import { deleteHealthFood, exportHealthFoodApi } from './api';
import {
deleteHealthFood,
exportHealthFoodApi,
syncHealthFoodPinyin,
} from './api';
import FormModalDemo from './components/modal.vue';
import ExcelUpload from './components/ExcelUpload.vue';
import CategoryModal from './components/CategoryModal.vue';
@@ -41,6 +46,7 @@ const PRODUCT_TYPE = 3;
// 控制顶部表格下拉操作按钮的显示状态
const hasTopTableDropDownActions = ref(false);
const syncing = ref(false);
// 定义表格事件监听器
const gridEvents: VxeGridListeners<any> = {
@@ -94,6 +100,40 @@ const [WarehousePriceModalComp, warehousePriceModalApi] = useVbenModal({
connectedComponent: WarehouseDrugPriceModal,
});
const [DrugDetailModalComp, drugDetailModalApi] = useVbenModal({
connectedComponent: DrugDetailModal,
});
/** 点击药名:打开详情 / 别名维护弹窗 */
function openDrugDetail(row: any) {
drugDetailModalApi.setData({
drug_id: row?.id,
gridApi,
});
drugDetailModalApi.open();
}
/** 同步本类型空拼音(首拼/全拼/数字) */
function handleSyncPinyin() {
AntModal.confirm({
title: '同步拼音',
content:
'将扫描本类型药品中首拼/全拼/数字串为空的记录并自动生成,已有值不会覆盖。确认执行?',
onOk: async () => {
syncing.value = true;
try {
const res = await syncHealthFoodPinyin();
message.success(
`同步完成:更新 ${Number(res?.updated || 0)} 条,跳过 ${Number(res?.skipped || 0)}`,
);
gridApi.query();
} finally {
syncing.value = false;
}
},
});
}
/**
* 打开某产品的配送仓绑定列表(带总仓售价供费用封顶/默认平台费)
* @param autoCreate 为 true 时打开后立刻弹出新增绑定表单
@@ -234,6 +274,7 @@ const openExcelUploadModal = () => {
<Page auto-content-height title="保健食品管理">
<ExcelUploadModal />
<FormModal />
<DrugDetailModalComp />
<CategoryModalComp />
<BindListModalComp />
<CentralStockInModalComp />
@@ -281,9 +322,12 @@ const openExcelUploadModal = () => {
</Button>
</template>
</TableAction>
<Button class="ml-2" :loading="syncing" @click="handleSyncPinyin">
同步拼音
</Button>
</template>
<template #product_info="{ row }">
<ProductInfoCell :row="row" />
<ProductInfoCell :row="row" @name-click="openDrugDetail" />
</template>
<template #category_warehouse="{ row }">
<CategoryWarehouseCell

View File

@@ -73,6 +73,13 @@ export async function updateZoneCategory(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}update-zone-category`, data);
}
/**
* 同步空拼音(首拼/全拼/数字串)
*/
export async function syncMedicalDevicePinyin() {
return requestClient.post<any>(`${prefix}sync-pinyin`, {});
}

View File

@@ -5,7 +5,7 @@ import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Button, message, Tag } from 'ant-design-vue';
import { Button, Modal as AntModal, message, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
@@ -13,13 +13,18 @@ import { downloadByData } from '#/util/tool';
import CategoryWarehouseCell from '#/views/business/product/_shared/category-warehouse-cell.vue';
import CentralPriceCell from '#/views/business/product/_shared/central-price-cell.vue';
import CentralStockInModal from '#/views/business/product/_shared/central-stock-in-modal.vue';
import DrugDetailModal from '#/views/business/product/_shared/drug-detail-modal.vue';
import DrugFreeShippingCell from '#/views/business/product/_shared/drug-free-shipping-cell.vue';
import ProductInfoCell from '#/views/business/product/_shared/product-info-cell.vue';
import WarehouseDrugPriceModal from '#/views/business/warehouse-drug-management/admin/components/modal.vue';
import BindListModal from '#/views/system/delivery-warehouse-drug/components/bind-list-modal.vue';
import { resolveCentralPrice } from '#/views/system/delivery-warehouse-drug/utils/resolve-central-price';
import { deleteMedicalDevice, exportMedicalDeviceApi } from './api';
import {
deleteMedicalDevice,
exportMedicalDeviceApi,
syncMedicalDevicePinyin,
} from './api';
import FormModalDemo from './components/modal.vue';
import ExcelUpload from './components/ExcelUpload.vue';
import CategoryModal from './components/CategoryModal.vue';
@@ -30,6 +35,7 @@ import { gridOptions } from './config/table';
const PRODUCT_TYPE = 7;
const hasTopTableDropDownActions = ref(false);
const syncing = ref(false);
const gridEvents: VxeGridListeners<any> = {
checkboxChange() {
@@ -72,6 +78,40 @@ const [WarehousePriceModalComp, warehousePriceModalApi] = useVbenModal({
connectedComponent: WarehouseDrugPriceModal,
});
const [DrugDetailModalComp, drugDetailModalApi] = useVbenModal({
connectedComponent: DrugDetailModal,
});
/** 点击药名:打开详情 / 别名维护弹窗 */
function openDrugDetail(row: any) {
drugDetailModalApi.setData({
drug_id: row?.id,
gridApi,
});
drugDetailModalApi.open();
}
/** 同步本类型空拼音(首拼/全拼/数字) */
function handleSyncPinyin() {
AntModal.confirm({
title: '同步拼音',
content:
'将扫描本类型药品中首拼/全拼/数字串为空的记录并自动生成,已有值不会覆盖。确认执行?',
onOk: async () => {
syncing.value = true;
try {
const res = await syncMedicalDevicePinyin();
message.success(
`同步完成:更新 ${Number(res?.updated || 0)} 条,跳过 ${Number(res?.skipped || 0)}`,
);
gridApi.query();
} finally {
syncing.value = false;
}
},
});
}
/**
* 打开某产品的配送仓绑定列表(带总仓售价供费用封顶/默认平台费)
* @param autoCreate 为 true 时打开后立刻弹出新增绑定表单
@@ -181,6 +221,7 @@ const openExcelUploadModal = () => {
<Page auto-content-height title="医疗器械管理">
<ExcelUploadModal />
<FormModal />
<DrugDetailModalComp />
<CategoryModalComp />
<BindListModalComp />
<CentralStockInModalComp />
@@ -228,9 +269,12 @@ const openExcelUploadModal = () => {
</Button>
</template>
</TableAction>
<Button class="ml-2" :loading="syncing" @click="handleSyncPinyin">
同步拼音
</Button>
</template>
<template #product_info="{ row }">
<ProductInfoCell :row="row" />
<ProductInfoCell :row="row" @name-click="openDrugDetail" />
</template>
<template #category_warehouse="{ row }">
<CategoryWarehouseCell

View File

@@ -73,6 +73,13 @@ export async function updateZoneCategory(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}update-zone-category`, data);
}
/**
* 同步空拼音(首拼/全拼/数字串)
*/
export async function syncNonDrugPinyin() {
return requestClient.post<any>(`${prefix}sync-pinyin`, {});
}

View File

@@ -5,7 +5,7 @@ import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Button, message, Tag } from 'ant-design-vue';
import { Button, Modal as AntModal, message, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
@@ -13,13 +13,14 @@ import { downloadByData } from '#/util/tool';
import CategoryWarehouseCell from '#/views/business/product/_shared/category-warehouse-cell.vue';
import CentralPriceCell from '#/views/business/product/_shared/central-price-cell.vue';
import CentralStockInModal from '#/views/business/product/_shared/central-stock-in-modal.vue';
import DrugDetailModal from '#/views/business/product/_shared/drug-detail-modal.vue';
import DrugFreeShippingCell from '#/views/business/product/_shared/drug-free-shipping-cell.vue';
import ProductInfoCell from '#/views/business/product/_shared/product-info-cell.vue';
import WarehouseDrugPriceModal from '#/views/business/warehouse-drug-management/admin/components/modal.vue';
import BindListModal from '#/views/system/delivery-warehouse-drug/components/bind-list-modal.vue';
import { resolveCentralPrice } from '#/views/system/delivery-warehouse-drug/utils/resolve-central-price';
import { deleteNonDrug, exportNonDrugApi } from './api';
import { deleteNonDrug, exportNonDrugApi, syncNonDrugPinyin } from './api';
import FormModalDemo from './components/modal.vue';
import ExcelUpload from './components/ExcelUpload.vue';
import CategoryModal from './components/CategoryModal.vue';
@@ -30,6 +31,7 @@ import { gridOptions } from './config/table';
const PRODUCT_TYPE = 6;
const hasTopTableDropDownActions = ref(false);
const syncing = ref(false);
const gridEvents: VxeGridListeners<any> = {
checkboxChange() {
@@ -72,6 +74,40 @@ const [WarehousePriceModalComp, warehousePriceModalApi] = useVbenModal({
connectedComponent: WarehouseDrugPriceModal,
});
const [DrugDetailModalComp, drugDetailModalApi] = useVbenModal({
connectedComponent: DrugDetailModal,
});
/** 点击药名:打开详情 / 别名维护弹窗 */
function openDrugDetail(row: any) {
drugDetailModalApi.setData({
drug_id: row?.id,
gridApi,
});
drugDetailModalApi.open();
}
/** 同步本类型空拼音(首拼/全拼/数字) */
function handleSyncPinyin() {
AntModal.confirm({
title: '同步拼音',
content:
'将扫描本类型药品中首拼/全拼/数字串为空的记录并自动生成,已有值不会覆盖。确认执行?',
onOk: async () => {
syncing.value = true;
try {
const res = await syncNonDrugPinyin();
message.success(
`同步完成:更新 ${Number(res?.updated || 0)} 条,跳过 ${Number(res?.skipped || 0)}`,
);
gridApi.query();
} finally {
syncing.value = false;
}
},
});
}
/**
* 打开某产品的配送仓绑定列表(带总仓售价供费用封顶/默认平台费)
* @param autoCreate 为 true 时打开后立刻弹出新增绑定表单
@@ -181,6 +217,7 @@ const openExcelUploadModal = () => {
<Page auto-content-height title="非药品管理">
<ExcelUploadModal />
<FormModal />
<DrugDetailModalComp />
<CategoryModalComp />
<BindListModalComp />
<CentralStockInModalComp />
@@ -228,9 +265,12 @@ const openExcelUploadModal = () => {
</Button>
</template>
</TableAction>
<Button class="ml-2" :loading="syncing" @click="handleSyncPinyin">
同步拼音
</Button>
</template>
<template #product_info="{ row }">
<ProductInfoCell :row="row" />
<ProductInfoCell :row="row" @name-click="openDrugDetail" />
</template>
<template #category_warehouse="{ row }">
<CategoryWarehouseCell

View File

@@ -55,3 +55,10 @@ export async function deleteServicePack(data: Record<string, any>) {
export async function updateZoneCategory(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}update-zone-category`, data);
}
/**
* 同步空拼音(首拼/全拼/数字串)
*/
export async function syncServicePackPinyin() {
return requestClient.post<any>(`${prefix}sync-pinyin`, {});
}

View File

@@ -5,20 +5,21 @@ import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Button, message, Tag } from 'ant-design-vue';
import { Button, Modal as AntModal, message, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import CategoryWarehouseCell from '#/views/business/product/_shared/category-warehouse-cell.vue';
import CentralPriceCell from '#/views/business/product/_shared/central-price-cell.vue';
import CentralStockInModal from '#/views/business/product/_shared/central-stock-in-modal.vue';
import DrugDetailModal from '#/views/business/product/_shared/drug-detail-modal.vue';
import DrugFreeShippingCell from '#/views/business/product/_shared/drug-free-shipping-cell.vue';
import ProductInfoCell from '#/views/business/product/_shared/product-info-cell.vue';
import WarehouseDrugPriceModal from '#/views/business/warehouse-drug-management/admin/components/modal.vue';
import BindListModal from '#/views/system/delivery-warehouse-drug/components/bind-list-modal.vue';
import { resolveCentralPrice } from '#/views/system/delivery-warehouse-drug/utils/resolve-central-price';
import { deleteServicePack } from './api';
import { deleteServicePack, syncServicePackPinyin } from './api';
import FormModalDemo from './components/modal.vue';
import ZoneCategoryModal from './components/ZoneCategoryModal.vue';
import { formOptions } from './config/search';
@@ -28,6 +29,7 @@ import { gridOptions } from './config/table';
const PRODUCT_TYPE = 5;
const hasTopTableDropDownActions = ref(false);
const syncing = ref(false);
const gridEvents: VxeGridListeners<any> = {
checkboxChange() {
@@ -68,6 +70,40 @@ const [WarehousePriceModalComp, warehousePriceModalApi] = useVbenModal({
connectedComponent: WarehouseDrugPriceModal,
});
const [DrugDetailModalComp, drugDetailModalApi] = useVbenModal({
connectedComponent: DrugDetailModal,
});
/** 点击药名:打开详情 / 别名维护弹窗 */
function openDrugDetail(row: any) {
drugDetailModalApi.setData({
drug_id: row?.id,
gridApi,
});
drugDetailModalApi.open();
}
/** 同步本类型空拼音(首拼/全拼/数字) */
function handleSyncPinyin() {
AntModal.confirm({
title: '同步拼音',
content:
'将扫描本类型药品中首拼/全拼/数字串为空的记录并自动生成,已有值不会覆盖。确认执行?',
onOk: async () => {
syncing.value = true;
try {
const res = await syncServicePackPinyin();
message.success(
`同步完成:更新 ${Number(res?.updated || 0)} 条,跳过 ${Number(res?.skipped || 0)}`,
);
gridApi.query();
} finally {
syncing.value = false;
}
},
});
}
/**
* 打开某产品的配送仓绑定列表(带总仓售价供费用封顶/默认平台费)
* @param autoCreate 为 true 时打开后立刻弹出新增绑定表单
@@ -164,6 +200,7 @@ const deleteApi = (row: any) => {
<template>
<Page auto-content-height title="产品服务包管理">
<FormModal />
<DrugDetailModalComp />
<ZoneCategoryModalComp />
<BindListModalComp />
<CentralStockInModalComp />
@@ -200,9 +237,12 @@ const deleteApi = (row: any) => {
</Button>
</template>
</TableAction>
<Button class="ml-2" :loading="syncing" @click="handleSyncPinyin">
同步拼音
</Button>
</template>
<template #product_info="{ row }">
<ProductInfoCell :row="row" />
<ProductInfoCell :row="row" @name-click="openDrugDetail" />
</template>
<template #category_warehouse="{ row }">
<CategoryWarehouseCell

View File

@@ -92,3 +92,10 @@ export async function updateMinAdjustPrice(data: {
export async function updateErpQtyFactor(data: { id: number; erp_qty_factor: number }) {
return requestClient.post<any>(`${prefix}update-erp-qty-factor`, data);
}
/**
* 同步空拼音(首拼/全拼/数字串)
*/
export async function syncWesternMedicinePinyin() {
return requestClient.post<any>(`${prefix}sync-pinyin`, {});
}

View File

@@ -134,6 +134,33 @@ export const modalFormProps: VbenFormProps = {
label: '药品别名',
rules: 'required',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '可留空,保存时按药名自动生成',
},
fieldName: 'pinyin_simple',
formItemClass: 'col-span-6',
label: '拼音首拼',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '可留空,保存时按药名自动生成',
},
fieldName: 'pinyin_full',
formItemClass: 'col-span-6',
label: '拼音全拼',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '可留空,保存时从药名提取数字',
},
fieldName: 'search_digits',
formItemClass: 'col-span-6',
label: '检索数字',
},
{
component: 'VbenInput',
componentProps: {

View File

@@ -5,7 +5,7 @@ import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Button, message, Tag } from 'ant-design-vue';
import { Button, Modal as AntModal, message, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
@@ -13,13 +13,18 @@ import { downloadByData } from '#/util/tool';
import CategoryWarehouseCell from '#/views/business/product/_shared/category-warehouse-cell.vue';
import CentralPriceCell from '#/views/business/product/_shared/central-price-cell.vue';
import CentralStockInModal from '#/views/business/product/_shared/central-stock-in-modal.vue';
import DrugDetailModal from '#/views/business/product/_shared/drug-detail-modal.vue';
import DrugFreeShippingCell from '#/views/business/product/_shared/drug-free-shipping-cell.vue';
import ProductInfoCell from '#/views/business/product/_shared/product-info-cell.vue';
import WarehouseDrugPriceModal from '#/views/business/warehouse-drug-management/admin/components/modal.vue';
import BindListModal from '#/views/system/delivery-warehouse-drug/components/bind-list-modal.vue';
import { resolveCentralPrice } from '#/views/system/delivery-warehouse-drug/utils/resolve-central-price';
import { deleteWesternMedicine, exportWesternMedicineApi } from './api';
import {
deleteWesternMedicine,
exportWesternMedicineApi,
syncWesternMedicinePinyin,
} from './api';
import FormModalDemo from './components/modal.vue';
import ExcelUpload from './components/ExcelUpload.vue';
import CategoryModal from './components/CategoryModal.vue';
@@ -32,6 +37,7 @@ import { gridOptions } from './config/table';
const PRODUCT_TYPE = 2;
const hasTopTableDropDownActions = ref(false);
const syncing = ref(false);
const gridEvents: VxeGridListeners<any> = {
checkboxChange() {
@@ -84,6 +90,40 @@ const [WarehousePriceModalComp, warehousePriceModalApi] = useVbenModal({
connectedComponent: WarehouseDrugPriceModal,
});
const [DrugDetailModalComp, drugDetailModalApi] = useVbenModal({
connectedComponent: DrugDetailModal,
});
/** 点击药名:打开详情 / 别名维护弹窗 */
function openDrugDetail(row: any) {
drugDetailModalApi.setData({
drug_id: row?.id,
gridApi,
});
drugDetailModalApi.open();
}
/** 同步本类型空拼音(首拼/全拼/数字) */
function handleSyncPinyin() {
AntModal.confirm({
title: '同步拼音',
content:
'将扫描本类型药品中首拼/全拼/数字串为空的记录并自动生成,已有值不会覆盖。确认执行?',
onOk: async () => {
syncing.value = true;
try {
const res = await syncWesternMedicinePinyin();
message.success(
`同步完成:更新 ${Number(res?.updated || 0)} 条,跳过 ${Number(res?.skipped || 0)}`,
);
gridApi.query();
} finally {
syncing.value = false;
}
},
});
}
/**
* 打开某药的配送仓绑定列表(带总仓售价供费用封顶/默认平台费)
* @param autoCreate 为 true 时打开后立刻弹出新增绑定表单
@@ -221,6 +261,7 @@ const openExcelUploadModal = () => {
<Page auto-content-height title="西(中成)药管理">
<ExcelUploadModal />
<FormModal />
<DrugDetailModalComp />
<CategoryModalComp />
<MinPriceModalComp />
<ErpQtyFactorModalComp />
@@ -274,9 +315,12 @@ const openExcelUploadModal = () => {
</Button>
</template>
</TableAction>
<Button class="ml-2" :loading="syncing" @click="handleSyncPinyin">
同步拼音
</Button>
</template>
<template #product_info="{ row }">
<ProductInfoCell :row="row" />
<ProductInfoCell :row="row" @name-click="openDrugDetail" />
</template>
<template #category_warehouse="{ row }">
<CategoryWarehouseCell

View File

@@ -25,8 +25,8 @@ export function createGridOptions(productType: number): VxeGridProps<RowType> {
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'id', align: 'left', title: 'ID', width: 100 },
{ field: 'drug.drug_name', align: 'left', title: '药品名称' },
{ field: 'drug.pinyin_simple', title: '拼音' },
{ field: 'drug.drug_name', align: 'left', title: '药品名称', minWidth: 160, slots: { default: 'drug_name' } },
{ field: 'drug.pinyin_simple', title: '拼音', minWidth: 120, slots: { default: 'drug_pinyin' } },
{
field: 'image',
align: 'left',

View File

@@ -13,6 +13,7 @@ import { TableAction } from '#/components/table-action';
import { downloadByData } from '#/util/tool';
import { useWarehouseDrugTypeRoute } from '../shared/useWarehouseDrugTypeRoute';
import WarehouseDrugNameCell from '../shared/components/WarehouseDrugNameCell.vue';
import {
deleteWarehouseDrugManagement,
getWarehouseExportDataApi,
@@ -286,6 +287,20 @@ function onSpreadsheetConfigSaved() {
</template>
</TableAction>
</template>
<!-- 药名/拼音搜索命中别名时展示匹配别名对齐开方 -->
<template #drug_name="{ row }">
<WarehouseDrugNameCell :row="row" />
</template>
<template #drug_pinyin="{ row }">
<span>
{{
row.matched_alias_pinyin ||
row.drug?.matched_alias_pinyin ||
row.drug?.pinyin_simple ||
'-'
}}
</span>
</template>
<template #image="{ row }">
<Image
v-if="rowProductImageSrc(row)"

View File

@@ -0,0 +1,58 @@
<script lang="ts" setup>
/**
* 仓库/药品列表药名单元格:搜索命中别名时展示「别名 + 拼音」(对齐开方气泡)
*/
defineProps<{
row: Record<string, any>;
}>();
function drugNameOf(row: Record<string, any>): string {
return String(row?.drug?.drug_name || row?.drug_name || '').trim() || '-';
}
function matchedAliasOf(row: Record<string, any>): string {
return String(
row?.matched_alias || row?.drug?.matched_alias || '',
).trim();
}
function pinyinOf(row: Record<string, any>): string {
const aliasPy = String(
row?.matched_alias_pinyin || row?.drug?.matched_alias_pinyin || '',
).trim();
if (matchedAliasOf(row) && aliasPy) return aliasPy;
return String(row?.drug?.pinyin_simple || row?.pinyin_simple || '').trim();
}
</script>
<template>
<div class="wh-drug-name-cell">
<div class="wh-drug-name-cell__name" :title="drugNameOf(row)">
{{ drugNameOf(row) }}
</div>
<div v-if="matchedAliasOf(row)" class="wh-drug-name-cell__alias">
别名{{ matchedAliasOf(row) }}
<span v-if="pinyinOf(row)" class="wh-drug-name-cell__py">
{{ pinyinOf(row) }}
</span>
</div>
</div>
</template>
<style scoped>
.wh-drug-name-cell__name {
font-weight: 500;
line-height: 1.35;
word-break: break-all;
}
.wh-drug-name-cell__alias {
margin-top: 2px;
font-size: 12px;
color: hsl(var(--muted-foreground));
line-height: 1.3;
word-break: break-all;
}
.wh-drug-name-cell__py {
color: hsl(var(--muted-foreground) / 0.9);
}
</style>

View File

@@ -25,8 +25,8 @@ export function createGridOptions(productType: number): VxeGridProps<RowType> {
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'id', align: 'left', title: 'ID', width: 100 },
{ field: 'drug.drug_name', align: 'left', title: '药品名称' },
{ field: 'drug.pinyin_simple', title: '拼音' },
{ field: 'drug.drug_name', align: 'left', title: '药品名称', minWidth: 160, slots: { default: 'drug_name' } },
{ field: 'drug.pinyin_simple', title: '拼音', minWidth: 120, slots: { default: 'drug_pinyin' } },
{
field: 'image',
align: 'left',

View File

@@ -12,6 +12,7 @@ import { TableAction } from '#/components/table-action';
import { downloadByData } from '#/util/tool';
import { useWarehouseDrugTypeRoute } from '../shared/useWarehouseDrugTypeRoute';
import WarehouseDrugNameCell from '../shared/components/WarehouseDrugNameCell.vue';
import {
checkSubscribeApi,
deleteWarehouseDrugManagementStore,
@@ -215,6 +216,20 @@ checkSubscribe();
</template>
</TableAction>
</template>
<!-- 药名/拼音搜索命中别名时展示匹配别名对齐开方 -->
<template #drug_name="{ row }">
<WarehouseDrugNameCell :row="row" />
</template>
<template #drug_pinyin="{ row }">
<span>
{{
row.matched_alias_pinyin ||
row.drug?.matched_alias_pinyin ||
row.drug?.pinyin_simple ||
'-'
}}
</span>
</template>
<template #image="{ row }">
<Image
:src="

View File

@@ -25,8 +25,8 @@ export function createGridOptions(productType: number): VxeGridProps<RowType> {
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'id', align: 'left', title: 'ID', width: 100 },
{ field: 'drug.drug_name', align: 'left', title: '药品名称' },
{ field: 'drug.pinyin_simple', title: '拼音' },
{ field: 'drug.drug_name', align: 'left', title: '药品名称', minWidth: 160, slots: { default: 'drug_name' } },
{ field: 'drug.pinyin_simple', title: '拼音', minWidth: 120, slots: { default: 'drug_pinyin' } },
{
field: 'image',
align: 'left',

View File

@@ -12,6 +12,7 @@ import { TableAction } from '#/components/table-action';
import { downloadByData } from '#/util/tool';
import { useWarehouseDrugTypeRoute } from '../shared/useWarehouseDrugTypeRoute';
import WarehouseDrugNameCell from '../shared/components/WarehouseDrugNameCell.vue';
import {
checkSubscribeApi,
deleteWarehouseDrugManagementStore,
@@ -234,6 +235,20 @@ checkSubscribe();
</template>
</TableAction>
</template>
<!-- 药名/拼音搜索命中别名时展示匹配别名对齐开方 -->
<template #drug_name="{ row }">
<WarehouseDrugNameCell :row="row" />
</template>
<template #drug_pinyin="{ row }">
<span>
{{
row.matched_alias_pinyin ||
row.drug?.matched_alias_pinyin ||
row.drug?.pinyin_simple ||
'-'
}}
</span>
</template>
<template #image="{ row }">
<Image
:src="

View File

@@ -567,12 +567,18 @@ function openFieldExpand(fieldCode: string, label: string) {
);
}
/** 保存病历(同步诊断/医嘱) */
/** 保存病历(同步诊断/医嘱;主诉必填 */
async function handleSave() {
if (!props.registerId) {
message.warning('无挂号信息');
return;
}
const chief = String(form.chief_complaint || '').trim();
if (!chief) {
message.warning('请填写主诉');
focusModuleTextarea('chief_complaint');
return;
}
loading.value = true;
try {
await saveMedicalRecord({
@@ -583,6 +589,8 @@ async function handleSave() {
diagnosis: diagnosisModel.value,
doctor_order: medicalAdviceModel.value,
medicalAdvice: medicalAdviceModel.value,
// 后端按此开关做主诉必填拦截
validate_required: 1,
});
persistLocalDraft();
message.success('病历已保存');
@@ -742,12 +750,14 @@ function fieldBlock(
label: string,
textKey?: string,
source?: 'entry' | 'tcm_disease' | 'tcm_syndrome' | 'tcm_method',
required = false,
) {
return {
fieldCode,
label,
textKey: textKey || fieldCode,
source: source || 'entry',
required,
};
}
@@ -772,7 +782,7 @@ const tcmCaseSubRows = [
* (中医病案单独置底全宽,不放进网格)
*/
const allNormalFields = [
fieldBlock('chief_complaint', '主诉'),
fieldBlock('chief_complaint', '主诉', undefined, undefined, true),
fieldBlock('family_history', '家族史'),
fieldBlock('present_illness', '现病史'),
fieldBlock('epidemic_history', '流行病学史'),
@@ -848,7 +858,10 @@ const visibleNormalFields = computed(() =>
class="mr-field-block"
>
<div class="mr-field-head mb-1">
<span class="mr-field-label">{{ f.label }}</span>
<span class="mr-field-label">
{{ f.label }}
<em v-if="f.required" class="mr-field-required">*</em>
</span>
<!-- 诊断/医嘱走处方同源气泡其余走病历词条 -->
<div
v-if="f.fieldCode === 'diagnosis'"
@@ -1265,6 +1278,12 @@ const visibleNormalFields = computed(() =>
white-space: nowrap;
color: hsl(var(--foreground));
}
.mr-field-required {
font-style: normal;
font-weight: 700;
color: #cf1322;
margin-left: 2px;
}
/* 一级 label 前置色条:醒目分区 */
.mr-field-label::before {
content: '';

View File

@@ -281,8 +281,10 @@ function updatePanelPlacement() {
listViewportH.value = Math.max(100, panelMax - HINT_HEIGHT - HEAD_HEIGHT);
const PANEL_MIN_WIDTH = 560;
// 在视口内尽量加宽,减少名称/词条正文被省略
const PANEL_PREFERRED_WIDTH = Math.min(920, vw - PANEL_PAD * 2);
const width = Math.min(
Math.max(rect.width, PANEL_MIN_WIDTH),
Math.max(rect.width, PANEL_MIN_WIDTH, PANEL_PREFERRED_WIDTH),
vw - PANEL_PAD * 2,
);
let left = rect.left;
@@ -1190,8 +1192,9 @@ defineExpose({
.entry-keyword-bubble__head,
.entry-keyword-bubble__row {
display: grid;
grid-template-columns: 90px 120px 1fr 72px;
gap: 6px;
/* 名称/拼音/正文加宽,备注收窄,减少词条正文被裁切 */
grid-template-columns: minmax(130px, 1.1fr) minmax(150px, 1.2fr) minmax(200px, 2.2fr) minmax(88px, 0.9fr);
gap: 8px;
padding: 6px 12px;
font-size: 13px;
align-items: center;
@@ -1199,15 +1202,15 @@ defineExpose({
}
.entry-keyword-bubble__head.has-score,
.entry-keyword-bubble__row.has-score {
grid-template-columns: 84px 110px 1fr 56px 48px;
grid-template-columns: minmax(120px, 1fr) minmax(140px, 1.1fr) minmax(180px, 2fr) minmax(72px, 0.8fr) 52px;
}
.entry-keyword-bubble__head.has-action,
.entry-keyword-bubble__row.has-action {
grid-template-columns: 84px 110px 1fr 56px 72px;
grid-template-columns: minmax(120px, 1fr) minmax(140px, 1.1fr) minmax(180px, 2fr) minmax(72px, 0.8fr) 76px;
}
.entry-keyword-bubble__head.has-score.has-action,
.entry-keyword-bubble__row.has-score.has-action {
grid-template-columns: 80px 100px 1fr 52px 44px 68px;
grid-template-columns: minmax(110px, 0.95fr) minmax(130px, 1fr) minmax(160px, 1.9fr) minmax(64px, 0.7fr) 48px 72px;
}
.entry-keyword-bubble__head {
flex-shrink: 0;
@@ -1226,7 +1229,6 @@ defineExpose({
color: hsl(var(--accent-foreground));
}
.col-title,
.col-content,
.col-remark,
.col-py {
overflow: hidden;
@@ -1236,6 +1238,17 @@ defineExpose({
.col-title {
font-weight: 500;
}
/* 词条正文:最多两行,配合加宽面板尽量多露字 */
.col-content {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
white-space: normal;
word-break: break-word;
line-height: 1.35;
}
.col-py {
font-size: 12px;
color: hsl(var(--muted-foreground));