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
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:
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as WarehouseAvailableDrugSearch } from './warehouse-available-drug-search.vue';
|
||||
export type { WarehouseDrugItem } from './warehouse-available-drug-search.vue';
|
||||
@@ -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>
|
||||
@@ -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 }[]>(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user