1. 对账单、订单的改良
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CI / CI OK (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled

This commit is contained in:
李琦
2026-05-18 09:40:35 +08:00
parent 56ac91c4af
commit 0b1b08c7d7
6 changed files with 416 additions and 94 deletions

View File

@@ -1,20 +1,31 @@
<script lang="ts" setup>
/**
* 超级仓库 Admin选品字段与常用方「药品搜索」一致DrugSearchSelect
* 超级仓库 Admin选品字段搜索尚未加入总仓yii_drugstore_drug的药品
*/
import { computed, ref, watch } from 'vue';
import { useUserStore } from '@vben/stores';
import { Button } from 'ant-design-vue';
import { DrugSearchSelect } from '#/components/drug-search-select';
import {
WarehouseAvailableDrugSearch,
type WarehouseDrugItem,
} from '#/components/warehouse-drug-search';
const PRODUCT_TYPE_LABEL: Record<number, string> = {
1: '中药',
2: '西药',
3: '保健食品',
4: '中成药',
5: '产品服务包',
6: '非药品',
7: '医疗器械',
};
const props = withDefaults(
defineProps<{
/** vben-form药品 ID */
value?: number;
/** 列表筛选类型1 中药2 西药 */
/** 列表筛选类型ProductTypeEnum */
productType?: number;
}>(),
{
@@ -26,17 +37,11 @@ const emit = defineEmits<{
'update:value': [value: number | undefined];
}>();
const userStore = useUserStore();
const selectedLabel = ref('');
const searchType = computed(() => (props.productType === 2 ? 2 : 1));
const storeId = computed(() => userStore.userInfo?.store_id ?? 2);
const placeholder = computed(() => {
const name = props.productType === 2 ? '西药' : '药';
return `输入${name}名称搜索(显示图片、供应商、规格、价格`;
const name = PRODUCT_TYPE_LABEL[props.productType] ?? '药';
return `输入${name}名称或拼音搜索(显示图片、供应商、规格)`;
});
watch(
@@ -49,11 +54,9 @@ watch(
{ immediate: true },
);
function onSelect(drug: any) {
const id = drug._drugId ?? drug.drug?.id ?? drug.drug_id;
selectedLabel.value =
drug._drugName || drug.drug?.drug_name || drug.drug_name || '';
emit('update:value', id);
function onSelect(drug: WarehouseDrugItem) {
selectedLabel.value = drug.drug_name || '';
emit('update:value', drug.id);
}
function clearSelection() {
@@ -64,9 +67,8 @@ function clearSelection() {
<template>
<div class="w-full space-y-2">
<DrugSearchSelect
:type="searchType"
:store-id="storeId"
<WarehouseAvailableDrugSearch
:product-type="productType"
:placeholder="placeholder"
@select="onSelect"
/>

View File

@@ -0,0 +1,2 @@
export { default as WarehouseAvailableDrugSearch } from './warehouse-available-drug-search.vue';
export type { WarehouseDrugItem } from './warehouse-available-drug-search.vue';

View File

@@ -0,0 +1,343 @@
<script lang="ts" setup>
/**
* 总仓库新增搜索尚未加入总仓yii_drugstore_drug的药品
*/
import { ref, watch, onMounted, onUnmounted } from 'vue';
import { SearchOutlined, LoadingOutlined } from '@ant-design/icons-vue';
import { Input, Spin } from 'ant-design-vue';
import { debounce } from 'lodash-es';
import { searchAvailableWarehouseDrugs } from '#/views/business/warehouse-drug-management/admin/api';
const TCM_PLACEHOLDER_IMAGE =
'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk_upload_20250510/cd470ebf97e31c4048ba502ba71b66a3.jpg';
const PRODUCT_TYPE_LABEL: Record<number, string> = {
1: '中药',
2: '西药',
3: '保健食品',
4: '中成药',
5: '产品服务包',
6: '非药品',
7: '医疗器械',
};
interface Props {
productType?: number;
placeholder?: string;
disabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
productType: 1,
placeholder: '',
disabled: false,
});
const emit = defineEmits<{
(e: 'select', drug: WarehouseDrugItem): void;
}>();
export interface WarehouseDrugItem {
id: number;
drug_name: string;
pinyin_simple?: string;
specification?: string;
image?: string;
type?: number;
supplier?: { id?: number; name?: string };
}
const searchKeyword = ref('');
const isSearching = ref(false);
const searchResults = ref<WarehouseDrugItem[]>([]);
const showDropdown = ref(false);
const containerRef = ref<HTMLElement | null>(null);
const highlightIndex = ref(-1);
function defaultPlaceholder() {
const label = PRODUCT_TYPE_LABEL[props.productType] ?? '药品';
return `输入${label}名称或拼音搜索`;
}
function resolveDropdownImage(item: WarehouseDrugItem): string {
if (item.image) return item.image;
if (props.productType === 1) return TCM_PLACEHOLDER_IMAGE;
return '';
}
const searchDrugs = debounce(async (keyword: string) => {
if (!keyword || keyword.length < 1) {
searchResults.value = [];
showDropdown.value = false;
return;
}
isSearching.value = true;
showDropdown.value = true;
try {
const res = await searchAvailableWarehouseDrugs({
name: keyword,
type: props.productType,
});
searchResults.value = Array.isArray(res) ? res : [];
} catch {
searchResults.value = [];
} finally {
isSearching.value = false;
}
}, 300);
function handleInput(e: Event) {
const target = e.target as HTMLInputElement;
searchKeyword.value = target.value;
highlightIndex.value = -1;
searchDrugs(target.value);
}
function handleFocus() {
if (searchResults.value.length > 0) {
showDropdown.value = true;
}
}
function handleSelectDrug(drug: WarehouseDrugItem) {
emit('select', drug);
searchKeyword.value = '';
searchResults.value = [];
showDropdown.value = false;
highlightIndex.value = -1;
}
function handleKeydown(e: KeyboardEvent) {
if (!showDropdown.value || searchResults.value.length === 0) return;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
highlightIndex.value = Math.min(
highlightIndex.value + 1,
searchResults.value.length - 1,
);
break;
case 'ArrowUp':
e.preventDefault();
highlightIndex.value = Math.max(highlightIndex.value - 1, 0);
break;
case 'Enter':
e.preventDefault();
if (highlightIndex.value >= 0) {
handleSelectDrug(searchResults.value[highlightIndex.value]);
}
break;
case 'Escape':
showDropdown.value = false;
highlightIndex.value = -1;
break;
}
}
function handleClickOutside(e: MouseEvent) {
if (containerRef.value && !containerRef.value.contains(e.target as Node)) {
showDropdown.value = false;
highlightIndex.value = -1;
}
}
onMounted(() => {
document.addEventListener('click', handleClickOutside);
});
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside);
});
watch(
() => props.productType,
() => {
searchResults.value = [];
showDropdown.value = false;
searchKeyword.value = '';
},
);
</script>
<template>
<div ref="containerRef" class="warehouse-drug-search">
<Input
v-model:value="searchKeyword"
:placeholder="placeholder || defaultPlaceholder()"
:disabled="disabled"
allow-clear
@input="handleInput"
@focus="handleFocus"
@keydown="handleKeydown"
>
<template #prefix>
<LoadingOutlined v-if="isSearching" class="text-gray-400" />
<SearchOutlined v-else class="text-gray-400" />
</template>
</Input>
<div v-if="showDropdown" class="warehouse-drug-dropdown">
<div v-if="isSearching" class="warehouse-drug-dropdown__loading">
<Spin size="small" />
<span>搜索中...</span>
</div>
<div
v-else-if="searchResults.length === 0"
class="warehouse-drug-dropdown__empty"
>
暂无匹配药品或已在总仓库中
</div>
<div v-else class="warehouse-drug-dropdown__list">
<div
v-for="(item, index) in searchResults"
:key="item.id"
class="warehouse-drug-item"
:class="{ 'warehouse-drug-item--active': index === highlightIndex }"
@click="handleSelectDrug(item)"
@mouseenter="highlightIndex = index"
>
<div class="warehouse-drug-item__image">
<img
v-if="resolveDropdownImage(item)"
:src="resolveDropdownImage(item)"
alt=""
class="warehouse-drug-item__img"
@error="(e) => ((e.target as HTMLImageElement).style.display = 'none')"
/>
<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__meta">
<span v-if="item.specification" class="warehouse-drug-item__spec">
规格{{ item.specification }}
</span>
<span v-if="item.supplier?.name" class="warehouse-drug-item__supplier">
供应商{{ item.supplier.name }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.warehouse-drug-search {
position: relative;
width: 100%;
}
.warehouse-drug-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1050;
margin-top: 4px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow:
0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 9px 28px 8px rgba(0, 0, 0, 0.05);
max-height: 400px;
overflow: hidden;
&__loading,
&__empty {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 24px;
color: #999;
font-size: 14px;
}
&__list {
max-height: 400px;
overflow-y: auto;
padding: 4px 0;
}
}
.warehouse-drug-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
cursor: pointer;
transition: background-color 0.2s;
&:hover,
&--active {
background-color: #f5f7fa;
}
&__image {
flex-shrink: 0;
width: 48px;
height: 48px;
}
&__img {
width: 48px;
height: 48px;
object-fit: cover;
border-radius: 6px;
border: 1px solid #f0f0f0;
}
&__img-placeholder {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
border-radius: 6px;
font-size: 12px;
color: #bbb;
}
&__info {
flex: 1;
min-width: 0;
overflow: hidden;
}
&__name {
font-size: 14px;
font-weight: 500;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 4px;
}
&__meta {
display: flex;
flex-wrap: wrap;
gap: 12px;
font-size: 12px;
color: #999;
}
&__spec {
color: #666;
}
&__supplier {
color: #1890ff;
}
}
</style>

View File

@@ -18,6 +18,16 @@ export async function getWarehouseDrugManagementOption(data: any) {
return requestClient.get<any>(`${prefix}option`, { params: data });
}
/** 搜索可加入总仓的药品yii_drugstore_drug 中尚不存在) */
export async function searchAvailableWarehouseDrugs(params: {
name: string;
type: number;
}) {
return requestClient.get<any[]>(`${prefix}search-available-drugs`, {
params,
});
}
/** 仓库筛选:商品类型(与后端 ProductTypeEnum 一致) */
export async function getWarehouseProductTypeOptions() {
return requestClient.get<{ label: string; value: number }[]>(

View File

@@ -11,43 +11,40 @@ import {
createWarehouseDrugManagement,
updateWarehouseDrugManagement,
} from '../api';
import {
modalFormProps,
warehouseAdminDrugIdApiSelectProps,
} from '../config/form';
import { modalFormProps } from '../config/form';
import { getDrugUseList } from '#/views/doctor/doctor-reception/api';
const drugTime = ref([]);
const drugType = ref([]);
const drugUnit = ref([]);
const drugFrequency = ref([]);
getDrugUseList().then((res) => {
drugTime.value = res.drug_time.map((item) => {
return {
label: item.name,
value: item.id,
};
});
drugType.value = res.drug_use_type.map((item) => {
return {
label: item.name,
value: item.id,
};
});
drugUnit.value = res.drug_unit.map((item) => {
return {
label: item.name,
value: item.id,
};
});
drugFrequency.value = res.drug_use_frequency.map((item) => {
return {
label: item.name,
value: item.id,
};
});
return res;
});
// getDrugUseList().then((res) => {
// drugTime.value = res.drug_time.map((item) => {
// return {
// label: item.name,
// value: item.id,
// };
// });
// drugType.value = res.drug_use_type.map((item) => {
// return {
// label: item.name,
// value: item.id,
// };
// });
// drugUnit.value = res.drug_unit.map((item) => {
// return {
// label: item.name,
// value: item.id,
// };
// });
// drugFrequency.value = res.drug_use_frequency.map((item) => {
// return {
// label: item.name,
// value: item.id,
// };
// });
// return res;
// });
const isUpdate = ref(false);
const gridApi = ref();
@@ -93,27 +90,15 @@ const [Modal, modalApi] = useVbenModal({
}>();
const listDrugType = modalData?.listDrugType ?? 1;
const drugIdSchemaPatch =
listDrugType === 5
? {
fieldName: 'drug_id',
component: 'ApiSelect' as const,
componentProps: {
class: 'w-full',
...warehouseAdminDrugIdApiSelectProps,
},
}
: {
fieldName: 'drug_id',
component: 'WarehouseAdminDrugSearch' as const,
componentProps: {
class: 'w-full',
productType: listDrugType,
},
};
formApi.updateSchema([
drugIdSchemaPatch,
{
fieldName: 'drug_id',
component: 'WarehouseAdminDrugSearch' as const,
componentProps: {
class: 'w-full',
productType: listDrugType,
},
},
{
componentProps: {
options: drugTime.value,
@@ -150,8 +135,8 @@ const [Modal, modalApi] = useVbenModal({
</script>
<template>
<Modal
:title="`${isUpdate === true ? '编辑' : '新增'}西(中成)药`"
class="w-[30%]"
:title="`${isUpdate === true ? '编辑' : '新增'}仓库药品`"
class="w-[60%] h-[60%]"
>
<Form />
</Modal>

View File

@@ -1,25 +1,5 @@
import type { VbenFormProps } from '#/adapter/form';
import {
getWarehouseDrugManagementOption,
} from '#/views/business/warehouse-drug-management/admin/api';
/** 服务包等场景:总仓 option 下拉(与原先 ApiSelect 一致) */
export const warehouseAdminDrugIdApiSelectProps = {
api: getWarehouseDrugManagementOption,
allowClear: true,
showSearch: true,
filterOption: (input: string, option: any) => {
return option?.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
},
afterFetch: (data: { drug_name: string; id: number }[]) => {
return data.map((item: any) => ({
label: `${item.drug_name}${item.pinyin_simple}`,
value: item.id,
}));
},
};
export const modalFormProps: VbenFormProps = {
wrapperClass: 'grid-cols-12', // 24栅格,
commonConfig: {