feat: 金方相关

feat: 患者信息身份证改成非必填
This commit is contained in:
李琦
2026-08-08 18:36:26 +08:00
parent c4e833a1c4
commit 335b3535d5
11 changed files with 1184 additions and 283 deletions

View File

@@ -1,6 +1,7 @@
<script lang="ts" setup>
/**
* 药品详情弹窗:展示基础信息;管理员/超管可逐条添加/删除别名
* 药品详情弹窗
* 一级 Tab基础信息原详情+别名)/ 仓库信息(总仓+门店改价)
*/
import { computed, ref } from 'vue';
@@ -15,10 +16,12 @@ import {
Input,
Popconfirm,
Spin,
Tabs,
Tag,
message,
} from 'ant-design-vue';
import DrugWarehousePanel from './drug-warehouse-panel.vue';
import {
addDrugAlias,
deleteDrugAlias,
@@ -43,6 +46,11 @@ const canManage = ref(false);
/** 新增输入框 */
const newAlias = ref('');
const gridApiRef = ref<any>(null);
/** 一级 Tab基础信息 / 仓库信息 */
const mainTab = ref('basic');
const warehousePanelRef = ref<InstanceType<typeof DrugWarehousePanel> | null>(
null,
);
const titleText = computed(
() => `药品详情${drug.value?.drug_name ? ` · ${drug.value.drug_name}` : ''}`,
@@ -50,6 +58,8 @@ const titleText = computed(
const aliasCountText = computed(() => `${aliases.value.length}`);
const drugId = computed(() => Number(drug.value?.id || 0));
/**
* 用接口结果回填本地状态
*/
@@ -60,13 +70,13 @@ function applyDetail(res: any) {
}
/**
* 加载详情
* 加载基础详情
*/
async function loadDetail(drugId: number) {
async function loadDetail(id: number) {
loading.value = true;
newAlias.value = '';
try {
const res = await getDrugAliasDetail(drugId);
const res = await getDrugAliasDetail(id);
applyDetail(res);
} catch (e) {
console.error(e);
@@ -79,7 +89,7 @@ async function loadDetail(drugId: number) {
}
const [Modal, modalApi] = useVbenModal({
class: 'w-[720px]',
class: 'w-[900px]',
draggable: true,
fullscreenButton: false,
showConfirmButton: false,
@@ -87,6 +97,7 @@ const [Modal, modalApi] = useVbenModal({
onOpenChange(isOpen: boolean) {
if (!isOpen) {
newAlias.value = '';
mainTab.value = 'basic';
return;
}
const data = modalApi.getData<{
@@ -96,12 +107,22 @@ const [Modal, modalApi] = useVbenModal({
}>();
gridApiRef.value = data?.gridApi || null;
const id = Number(data?.drug_id || data?.id || 0);
mainTab.value = 'basic';
if (id > 0) {
void loadDetail(id);
}
},
});
/**
* 切换到仓库信息时刷新一次,避免打开后数据过期
*/
function onMainTabChange(key: string | number) {
if (key === 'warehouse' && drugId.value > 0) {
warehousePanelRef.value?.reload?.();
}
}
/**
* 添加一条别名(支持一次输入多个,按分隔符拆)
*/
@@ -135,12 +156,12 @@ async function handleAddAlias() {
* 删除一条别名
*/
async function handleDeleteAlias(row: AliasRow) {
const drugId = Number(drug.value?.id || 0);
const id = Number(drug.value?.id || 0);
const aliasId = Number(row?.id || 0);
if (!drugId || !aliasId || !canManage.value) return;
if (!id || !aliasId || !canManage.value) return;
deletingId.value = aliasId;
try {
const res = await deleteDrugAlias(drugId, aliasId);
const res = await deleteDrugAlias(id, aliasId);
applyDetail(res);
message.success('已删除');
gridApiRef.value?.query?.();
@@ -171,90 +192,101 @@ async function handleDeleteAlias(row: AliasRow) {
</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>
<Tabs
v-model:active-key="mainTab"
class="mt-3"
@change="onMainTabChange"
>
<Tabs.TabPane key="basic" tab="基础信息">
<Descriptions bordered size="small" :column="2">
<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 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 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>
<Popconfirm
v-if="canManage"
title="确认删除该别名?"
ok-text="删除"
cancel-text="取消"
@confirm="handleDeleteAlias(a)"
>
<div v-if="canManage" class="drug-detail__alias-add">
<Input
v-model:value="newAlias"
allow-clear
placeholder="输入别名后回车添加;多个可用顿号/逗号分隔"
:disabled="adding"
@press-enter="handleAddAlias"
/>
<Button
type="link"
danger
size="small"
:loading="deletingId === Number(a.id || 0)"
type="primary"
class="ml-2"
:loading="adding"
@click="handleAddAlias"
>
删除
添加
</Button>
</Popconfirm>
</div>
</div>
</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>
</Tabs.TabPane>
<Tabs.TabPane key="warehouse" tab="仓库信息">
<DrugWarehousePanel ref="warehousePanelRef" :drug-id="drugId" />
</Tabs.TabPane>
</Tabs>
</div>
<Empty v-else-if="!loading" description="未找到药品" />
</Spin>

View File

@@ -0,0 +1,220 @@
<script lang="ts" setup>
/**
* 药品详情「仓库信息」面板
* 子 Tab总仓只读展示/ 门店(可改供货价与售价)
*/
import { onMounted, ref, watch } from 'vue';
import {
Button,
Descriptions,
DescriptionsItem,
Empty,
InputNumber,
Spin,
Table,
Tabs,
Tag,
message,
} from 'ant-design-vue';
import {
getDrugWarehouseInfo,
updateStorePricesByDrug,
} from '#/views/business/warehouse-drug-management/admin/api';
const props = defineProps<{
drugId: number;
}>();
const loading = ref(false);
const saving = ref(false);
const warehouseTab = ref('central');
const central = ref<Record<string, any> | null>(null);
const stores = ref<Record<string, any>[]>([]);
const deliveryBinds = ref<Record<string, any>[]>([]);
/**
* 拉取总仓/门店/配送仓绑定
*/
async function loadWarehouse() {
const id = Number(props.drugId || 0);
if (id < 1) {
central.value = null;
stores.value = [];
deliveryBinds.value = [];
return;
}
loading.value = true;
try {
const res = await getDrugWarehouseInfo(id);
central.value = res?.central || null;
stores.value = (res?.stores || []).map((s: any) => ({
...s,
_buy_price: Number(s.buy_price || 0),
_price: Number(s.price || 0),
_dirty: false,
}));
deliveryBinds.value = res?.delivery_binds || [];
} catch {
central.value = null;
stores.value = [];
deliveryBinds.value = [];
} finally {
loading.value = false;
}
}
function markDirty(row: Record<string, any>) {
row._dirty = true;
}
/**
* 保存已改动的门店价格
*/
async function saveStorePrices() {
const dirty = stores.value.filter((s) => s._dirty);
if (dirty.length === 0) {
message.info('没有需要保存的修改');
return;
}
for (const row of dirty) {
if (Number(row._buy_price) < 0 || Number(row._price) < 0) {
message.warning(`门店「${row.store_name}」价格不能为负`);
return;
}
}
saving.value = true;
try {
await updateStorePricesByDrug({
drug_id: Number(props.drugId),
items: dirty.map((s) => ({
id: Number(s.id),
buy_price: Number(s._buy_price),
price: Number(s._price),
})),
});
message.success(`已更新 ${dirty.length} 家门店价格`);
await loadWarehouse();
} finally {
saving.value = false;
}
}
const storeColumns = [
{ title: '门店', dataIndex: 'store_name', key: 'store_name', ellipsis: true },
{ title: '供货价', key: 'buy_price', width: 140 },
{ title: '售价', key: 'price', width: 140 },
{ title: '状态', key: 'status', width: 80 },
];
watch(
() => props.drugId,
() => {
void loadWarehouse();
},
);
onMounted(() => {
void loadWarehouse();
});
defineExpose({ reload: loadWarehouse });
</script>
<template>
<Spin :spinning="loading">
<Tabs v-model:active-key="warehouseTab" size="small">
<Tabs.TabPane key="central" tab="总仓">
<Empty
v-if="!central"
:image="Empty.PRESENTED_IMAGE_SIMPLE"
description="未入总仓"
/>
<Descriptions v-else bordered size="small" :column="2">
<DescriptionsItem label="总仓ID">
{{ central.id }}
</DescriptionsItem>
<DescriptionsItem label="状态">
<Tag :color="central.status === 2 ? 'green' : 'red'">
{{ central.status_txt || '-' }}
</Tag>
</DescriptionsItem>
<DescriptionsItem label="供货价">
¥{{ Number(central.market_price || 0).toFixed(4) }}
</DescriptionsItem>
<DescriptionsItem label="建议售价">
¥{{ Number(central.price || 0).toFixed(4) }}
</DescriptionsItem>
<DescriptionsItem label="库存">
{{ central.stock ?? '-' }}
</DescriptionsItem>
<DescriptionsItem label="更新时间">
{{ central.updated_at || '-' }}
</DescriptionsItem>
</Descriptions>
<div v-if="deliveryBinds.length" class="mt-3 text-xs" style="color: hsl(var(--muted-foreground))">
已绑定 {{ deliveryBinds.length }} 个配送仓改总仓售价时需重分配三费
</div>
</Tabs.TabPane>
<Tabs.TabPane key="stores" tab="门店">
<div class="mb-2 flex items-center justify-between">
<span class="text-xs" style="color: hsl(var(--muted-foreground))">
{{ stores.length }} 家门店修改后点保存
</span>
<Button
type="primary"
size="small"
:loading="saving"
:disabled="!stores.some((s) => s._dirty)"
@click="saveStorePrices"
>
保存改价
</Button>
</div>
<Empty
v-if="stores.length === 0"
:image="Empty.PRESENTED_IMAGE_SIMPLE"
description="暂无门店价格"
/>
<Table
v-else
size="small"
:pagination="false"
:columns="storeColumns"
:data-source="stores"
:scroll="{ y: 320 }"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'buy_price'">
<InputNumber
v-model:value="record._buy_price"
:min="0"
:precision="4"
class="w-full"
size="small"
@change="markDirty(record)"
/>
</template>
<template v-else-if="column.key === 'price'">
<InputNumber
v-model:value="record._price"
:min="0"
:precision="4"
class="w-full"
size="small"
@change="markDirty(record)"
/>
</template>
<template v-else-if="column.key === 'status'">
<Tag :color="record.status === 2 ? 'green' : 'default'">
{{ record.status === 2 ? '上架' : '下架' }}
</Tag>
</template>
</template>
</Table>
</Tabs.TabPane>
</Tabs>
</Spin>
</template>

View File

@@ -161,3 +161,34 @@ export async function applyBatchPriceApi(data: {
new_count: number;
}>(`${prefix}apply-batch-price`, data);
}
/** 药品仓库信息:总仓 + 门店价 + 配送仓绑定 */
export async function getDrugWarehouseInfo(drugId: number) {
return requestClient.get<any>(`${prefix}drug-warehouse-info`, {
params: { drug_id: drugId },
});
}
/** 按药品批量改门店供货价/售价 */
export async function updateStorePricesByDrug(data: {
drug_id: number;
items: Array<{ id: number; price: number; buy_price: number }>;
}) {
return requestClient.post<any>(`${prefix}update-store-prices-by-drug`, data);
}
/** 总仓改价并重分配配送仓三费 */
export async function updateWarehouseWithDeliveryFees(data: {
id: number;
drug_id?: number;
market_price: number;
price: number;
delivery_binds: Array<{
id: number;
quote: number;
promo_fee: number;
platform_fee: number;
}>;
}) {
return requestClient.post<any>(`${prefix}update-with-delivery-fees`, data);
}

View File

@@ -0,0 +1,216 @@
<script lang="ts" setup>
/**
* 总仓改价后:强制重分配配送仓三费
* 不可点击遮罩关闭,必须确认或取消
*/
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Alert, InputNumber, Table, message } from 'ant-design-vue';
import {
calcFeeSum,
calcPromoFee,
} from '#/views/system/delivery-warehouse-drug/config/form';
import { updateWarehouseWithDeliveryFees } from '../api';
type BindRow = {
id: number;
warehouse_id: number;
warehouse_name: string;
quote: number;
promo_fee: number;
platform_fee: number;
};
const rows = ref<BindRow[]>([]);
const salePrice = ref(0);
const marketPrice = ref(0);
const centralId = ref(0);
const drugId = ref(0);
const gridApi = ref<any>(null);
const onSuccess = ref<(() => void) | null>(null);
const saleText = computed(() => `¥${Number(salePrice.value || 0).toFixed(4)}`);
const columns = [
{ title: '配送仓', dataIndex: 'warehouse_name', key: 'warehouse_name', ellipsis: true },
{ title: '报价', key: 'quote', width: 130 },
{ title: '平台费', key: 'platform_fee', width: 130 },
{ title: '推广费', key: 'promo_fee', width: 130 },
{ title: '合计', key: 'sum', width: 110 },
];
/**
* 售价变化后自动回填推广费 = 售价 报价 平台费
*/
function syncPromo(row: BindRow) {
row.promo_fee = calcPromoFee({
sale_price: salePrice.value,
quote: row.quote,
platform_fee: row.platform_fee,
promo_fee: row.promo_fee,
});
}
function feeSum(row: BindRow) {
return Number(
calcFeeSum({
quote: row.quote,
promo_fee: row.promo_fee,
platform_fee: row.platform_fee,
}),
);
}
const [Modal, modalApi] = useVbenModal({
class: 'w-[820px]',
draggable: true,
fullscreenButton: false,
// 禁止点遮罩关闭,避免漏填三费
closeOnClickModal: false,
closeOnPressEscape: false,
confirmText: '确认并保存',
onCancel() {
modalApi.close();
},
async onConfirm() {
if (!rows.value.length) {
message.warning('没有需要重分配的配送仓');
return;
}
for (const row of rows.value) {
const sum = feeSum(row);
if (!(sum > 0)) {
message.warning(`配送仓「${row.warehouse_name}」三费合计必须大于0`);
return;
}
if (salePrice.value > 0 && sum - salePrice.value > 0.00005) {
message.warning(
`配送仓「${row.warehouse_name}」三费合计不能超过售价 ${saleText.value}`,
);
return;
}
}
modalApi.setState({ confirmLoading: true, loading: true });
try {
await updateWarehouseWithDeliveryFees({
id: centralId.value,
drug_id: drugId.value,
market_price: marketPrice.value,
price: salePrice.value,
delivery_binds: rows.value.map((r) => ({
id: r.id,
quote: Number(r.quote),
promo_fee: Number(r.promo_fee),
platform_fee: Number(r.platform_fee),
})),
});
message.success('已更新总仓价格并重分配配送仓三费');
gridApi.value?.query?.() ?? gridApi.value?.reload?.();
onSuccess.value?.();
modalApi.close();
} finally {
modalApi.setState({ confirmLoading: false, loading: false });
}
},
onOpenChange(isOpen: boolean) {
if (!isOpen) {
rows.value = [];
onSuccess.value = null;
return;
}
const data = modalApi.getData<Record<string, any>>() || {};
centralId.value = Number(data.central_id || data.id || 0);
drugId.value = Number(data.drug_id || 0);
salePrice.value = Number(data.price || 0);
marketPrice.value = Number(data.market_price || 0);
gridApi.value = data.gridApi || null;
onSuccess.value = data.onSuccess || null;
const binds = Array.isArray(data.delivery_binds) ? data.delivery_binds : [];
rows.value = binds.map((b: any) => {
const row: BindRow = {
id: Number(b.id),
warehouse_id: Number(b.warehouse_id),
warehouse_name: String(b.warehouse_name || `仓#${b.warehouse_id}`),
quote: Number(b.quote || 0),
platform_fee: Number(b.platform_fee || 0),
promo_fee: Number(b.promo_fee || 0),
};
// 按新售价重算推广费默认值
if (salePrice.value > 0) {
if (!(row.platform_fee > 0)) {
row.platform_fee = Number((salePrice.value * 0.05).toFixed(4));
}
syncPromo(row);
}
return row;
});
},
});
</script>
<template>
<Modal title="重分配配送仓三费">
<Alert
type="warning"
show-icon
class="mb-3"
message="该药品已绑定配送仓,总仓改价后必须重新填写各仓报价/平台费/推广费后才能保存"
:description="`新建议售价 ${saleText};推广费默认=售价−报价−平台费,可微调`"
/>
<Table
size="small"
:pagination="false"
:columns="columns"
:data-source="rows"
:scroll="{ y: 360 }"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'quote'">
<InputNumber
v-model:value="record.quote"
:min="0"
:precision="4"
class="w-full"
size="small"
@change="syncPromo(record)"
/>
</template>
<template v-else-if="column.key === 'platform_fee'">
<InputNumber
v-model:value="record.platform_fee"
:min="0"
:precision="4"
class="w-full"
size="small"
@change="syncPromo(record)"
/>
</template>
<template v-else-if="column.key === 'promo_fee'">
<InputNumber
v-model:value="record.promo_fee"
:min="0"
:precision="4"
class="w-full"
size="small"
/>
</template>
<template v-else-if="column.key === 'sum'">
<span
:style="
salePrice > 0 && feeSum(record) - salePrice > 0.00005
? { color: 'hsl(var(--destructive))' }
: undefined
"
>
¥{{ feeSum(record).toFixed(4) }}
</span>
</template>
</template>
</Table>
</Modal>
</template>

View File

@@ -1,4 +1,8 @@
<script lang="ts" setup>
/**
* 总仓药品新增/编辑
* 编辑且该药已绑配送仓时:打开不可点遮罩关闭的三费重分配确认窗
*/
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
@@ -6,51 +10,63 @@ import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { getDeliveryWarehouseDrugList } from '#/views/system/delivery-warehouse-drug/api';
import {
createWarehouseDrugManagement,
getWarehouseDrugManagementInfo,
updateWarehouseDrugManagement,
} from '../api';
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;
// });
import DeliveryFeeReallocateModalDemo from './delivery-fee-reallocate-modal.vue';
const isUpdate = ref(false);
const gridApi = ref();
const [Form, formApi] = useVbenForm(modalFormProps);
const [FeeModal, feeModalApi] = useVbenModal({
connectedComponent: DeliveryFeeReallocateModalDemo,
});
/**
* 查询该药品是否已绑定配送仓
*/
async function fetchDeliveryBinds(drugId: number) {
if (drugId < 1) return [];
const res = await getDeliveryWarehouseDrugList({
page: 1,
pageSize: 100,
drug_id: drugId,
});
return res?.items || [];
}
/**
* 普通保存(无配送仓绑定 / 新增)
*/
async function submitPlain(values: Record<string, any>) {
modalApi.setState({ loading: true, confirmLoading: true });
try {
const submitApi = isUpdate.value
? updateWarehouseDrugManagement
: createWarehouseDrugManagement;
await submitApi(values);
message.success('保存成功');
gridApi.value?.reload?.() ?? gridApi.value?.query?.();
modalApi.close();
} finally {
modalApi.setState({ loading: false, confirmLoading: false });
}
}
/**
* 打开三费重分配确认(禁止点遮罩关闭)
*/
function openFeeReallocate(payload: Record<string, any>) {
feeModalApi.setData(payload);
feeModalApi.open();
}
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
@@ -60,80 +76,87 @@ const [Modal, modalApi] = useVbenModal({
},
onConfirm: async () => {
formApi.validate().then(async (e: any) => {
if (e.valid) {
const values = await formApi.getValues();
modalApi.setState({ loading: true, confirmLoading: true });
const submitApi = isUpdate.value
? updateWarehouseDrugManagement
: createWarehouseDrugManagement;
submitApi(values)
.then(() => {
message.success('保存成功');
gridApi.value?.reload();
modalApi.close();
})
.finally(() => {
modalApi.setState({ loading: false, confirmLoading: false });
});
if (!e.valid) return;
const values = await formApi.getValues();
if (!isUpdate.value) {
await submitPlain(values);
return;
}
// 商品列表点总仓售价时可能只带了总仓 id补齐 drug_id
let drugId = Number(values.drug_id || 0);
modalApi.setState({ loading: true, confirmLoading: true });
try {
if (!drugId && values.id) {
const info = await getWarehouseDrugManagementInfo(Number(values.id));
drugId = Number(info?.drug_id || 0);
values.drug_id = drugId;
}
const binds = await fetchDeliveryBinds(drugId);
if (!binds.length) {
await submitPlain(values);
return;
}
modalApi.close();
openFeeReallocate({
id: Number(values.id),
central_id: Number(values.id),
drug_id: drugId,
market_price: Number(values.market_price),
price: Number(values.price),
delivery_binds: binds.map((b: any) => ({
id: b.id,
warehouse_id: b.warehouse_id,
warehouse_name:
b.warehouse?.name || b.warehouse_name || `仓#${b.warehouse_id}`,
quote: b.quote,
promo_fee: b.promo_fee,
platform_fee: b.platform_fee,
})),
gridApi: gridApi.value,
});
} catch (err) {
console.error(err);
} finally {
modalApi.setState({ loading: false, confirmLoading: false });
}
});
await formApi.validateAndSubmitForm();
},
onOpenChange(isOpen: boolean) {
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
if (isOpen) {
const modalData = modalApi.getData<{
gridApi?: unknown;
listDrugType?: number;
update?: boolean;
values?: Record<string, any>;
}>();
const listDrugType = modalData?.listDrugType ?? 1;
formApi.updateSchema([
{
fieldName: 'drug_id',
component: 'WarehouseAdminDrugSearch' as const,
componentProps: {
class: 'w-full',
productType: listDrugType,
},
if (!isOpen) return;
const modalData = modalApi.getData<{
gridApi?: unknown;
listDrugType?: number;
update?: boolean;
values?: Record<string, any>;
}>();
const listDrugType = modalData?.listDrugType ?? 1;
formApi.updateSchema([
{
fieldName: 'drug_id',
component: 'WarehouseAdminDrugSearch' as const,
componentProps: {
class: 'w-full',
productType: listDrugType,
},
{
componentProps: {
options: drugTime.value,
},
fieldName: 'time_id',
},
{
componentProps: {
options: drugUnit.value,
},
fieldName: 'unit_id',
},
{
componentProps: {
options: drugType.value,
},
fieldName: 'type_id',
},
{
componentProps: {
options: drugFrequency.value,
},
fieldName: 'frequency_id',
},
]);
const { values, update } = modalData ?? {};
if (values) {
isUpdate.value = !!update;
formApi.setValues(values);
}
},
]);
const { values, update } = modalData ?? {};
if (values) {
isUpdate.value = !!update;
formApi.setValues(values);
} else {
isUpdate.value = false;
formApi.resetForm();
}
},
});
</script>
<template>
<!-- FeeModal 与编辑窗平级避免编辑窗关闭后确认窗挂载异常 -->
<FeeModal />
<Modal
:title="`${isUpdate === true ? '编辑' : '新增'}仓库药品`"
class="w-[60%] h-[60%]"

View File

@@ -19,6 +19,9 @@ import {
getWarehouseExportDataApi,
updateWarehouseDrugManagementStatusApi
} from './api';
import BindListModalDemo from '#/views/system/delivery-warehouse-drug/components/bind-list-modal.vue';
import { resolveCentralPrice } from '#/views/system/delivery-warehouse-drug/utils/resolve-central-price';
import FormModalDemo from './components/modal.vue';
import ExcelUpload from './components/ExcelUpload.vue';
import SpreadsheetTableConfigModal from './components/SpreadsheetTableConfigModal.vue';
@@ -83,6 +86,10 @@ const [FormModal, formModalApi] = useVbenModal({
connectedComponent: FormModalDemo,
});
const [BindListModal, bindListModalApi] = useVbenModal({
connectedComponent: BindListModalDemo,
});
const [ExcelUploadModal, ExcelUploadModalApi] = useVbenModal({
connectedComponent: ExcelUpload,
});
@@ -111,6 +118,30 @@ function rowProductImageSrc(row: any) {
return drug?.image || '';
}
/**
* 打开某药品的配送仓绑定列表/新增
*/
function openBindList(row: Record<string, any>, autoCreate = false) {
const drug = row?.drug || {};
const drugId = Number(row?.drug_id || drug?.id || 0);
if (drugId < 1) {
message.warning('无法识别药品');
return;
}
bindListModalApi.setData({
drug_id: drugId,
drug_name: drug.drug_name || row.drug_name || '',
specification: drug.specification || '',
image: drug.image || '',
// ?? 与 || 混用必须加括号,否则 vue/compiler-sfc 打包报错
central_price: (resolveCentralPrice(row) ?? Number(row.price || 0)) || null,
values: row,
gridApi,
autoCreate,
});
bindListModalApi.open();
}
const showModal = (data = {}, isUpdate = false) => {
formModalApi.setData({
values: data,
@@ -195,6 +226,7 @@ function onSpreadsheetConfigSaved() {
<ExcelUploadModal />
<TcmPriceBatchModalComp />
<FormModal />
<BindListModal />
<SpreadsheetTableConfigModal
v-if="productType === 1"
ref="spreadsheetConfigModalRef"
@@ -339,6 +371,13 @@ function onSpreadsheetConfigSaved() {
// auth: ['western-medicine', 'sys:role:detail'],
onClick: showModal.bind(null, row, true),
},
{
label: '配送仓绑定',
type: 'link',
icon: 'mdi:truck-delivery-outline',
size: 'small',
onClick: openBindList.bind(null, row, false),
},
]"
:drop-down-actions="[
{

View File

@@ -803,6 +803,39 @@ function handleImportAiMedicalRecord(payload: {
const aiMedicalRecordModalRef = ref<InstanceType<typeof AiMedicalRecordModal> | null>(null);
/**
* AI「生成病历」弹窗点改字段即时同步到本页表单不落库与 AI 出方预览改病历同链路)
*/
function handleSyncAiMedicalRecord(partial: Record<string, any>) {
patchFields(partial || {});
}
/**
* 外部/弹窗局部回写病历字段,同步表单与本地草稿,不自动落库
*/
function patchFields(partial: Record<string, any>) {
if (!partial || typeof partial !== 'object') return;
Object.keys(partial).forEach((k) => {
const raw = partial[k];
const val = raw == null ? '' : String(raw);
if (k === 'diagnosis') {
form.diagnosis = val;
emit('update:diagnosis', val);
return;
}
if (k === 'doctor_order' || k === 'medicalAdvice') {
form.doctor_order = val;
emit('update:medicalAdvice', val);
return;
}
if (Object.prototype.hasOwnProperty.call(form, k)) {
(form as any)[k] = val;
}
});
persistLocalDraft();
scheduleSyncToPrescription();
}
async function handleClear() {
if (!props.registerId) return;
await clearMedicalRecord({
@@ -870,31 +903,8 @@ defineExpose({
diagnosis: diagnosisModel.value,
doctor_order: medicalAdviceModel.value,
}),
/**
* 外部局部回写病历字段(如 AI 出方预览里点改),同步表单与本地草稿,不自动落库
*/
patchFields: (partial: Record<string, any>) => {
if (!partial || typeof partial !== 'object') return;
Object.keys(partial).forEach((k) => {
const raw = partial[k];
const val = raw == null ? '' : String(raw);
if (k === 'diagnosis') {
form.diagnosis = val;
emit('update:diagnosis', val);
return;
}
if (k === 'doctor_order' || k === 'medicalAdvice') {
form.doctor_order = val;
emit('update:medicalAdvice', val);
return;
}
if (Object.prototype.hasOwnProperty.call(form, k)) {
(form as any)[k] = val;
}
});
persistLocalDraft();
scheduleSyncToPrescription();
},
/** 外部局部回写(如 AI 出方预览点改) */
patchFields,
});
/** source走中医三典气泡默认 entry 走病历词条 */
@@ -1455,6 +1465,7 @@ const visibleHealthFields = computed(() =>
<AiMedicalRecordModal
ref="aiMedicalRecordModalRef"
@import="handleImportAiMedicalRecord"
@sync-medical-record="handleSyncAiMedicalRecord"
/>
</div>
</template>

View File

@@ -16,9 +16,14 @@ import {
aiListGenerations,
} from '../api';
import { MEDICAL_RECORD_FIELD_OPTIONS } from '../config/constants';
import TextareaExpandModal from './TextareaExpandModal.vue';
import AiDisclaimerBanner from '#/views/doctor/components/AiDisclaimerBanner.vue';
import { ensureAiFeatureConsent } from '#/views/doctor/utils/aiFeatureGate';
import PrescriptionLoading from '#/components/loading/PrescriptionLoading.vue';
import { getSystemConfigByKeys } from '#/views/system/system-config/api';
/** 常用|我的展示位置(与病历面板一致,供放大弹窗 chips/气泡) */
type MrCommonDisplayMode = 'tags' | 'bubble' | 'both';
/** 本地「正在生成中」占位 id不与真实 generation_id 冲突 */
const PENDING_GEN_ID = -1;
@@ -93,6 +98,8 @@ const emit = defineEmits<{
} | null;
},
): void;
/** 生成弹窗里改字段时即时回写病历面板(与 AI 出方 sync-medical-record 同协议) */
(e: 'sync-medical-record', payload: Record<string, any>): void;
}>();
const registerId = ref(0);
@@ -136,6 +143,33 @@ const previewRows = computed(() =>
})).filter((r) => r.value !== ''),
);
/**
* 二级「生成病历」弹窗行:主诉始终展示 + 选填字段(空也展示,可点改)
* UI 与 AI 出方 GenModal 一致Descriptions 浏览态 / 点击进入 TextArea
*/
const genPreviewRows = computed(() => {
const rows: Array<{ key: string; label: string }> = [
{ key: 'chief_complaint', label: '主诉' },
];
for (const f of GEN_OPTIONAL_FIELDS) {
rows.push({ key: f.value, label: f.label });
}
return rows;
});
/** 二级弹窗:当前正在编辑的字段 key空表示浏览态 */
const editingKey = ref('');
/** 单击延迟定时器:与双击放大区分开,避免双击先误进行内编辑 */
let genFieldClickTimer: ReturnType<typeof setTimeout> | null = null;
const commonDisplayMode = ref<MrCommonDisplayMode>('tags');
const showCommonTags = computed(
() => commonDisplayMode.value === 'tags' || commonDisplayMode.value === 'both',
);
const showCommonInBubble = computed(
() =>
commonDisplayMode.value === 'bubble' || commonDisplayMode.value === 'both',
);
const [Drawer, drawerApi] = useVbenDrawer({
title: 'AI写病历',
class: 'w-[860px]',
@@ -188,6 +222,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
drawerApi.close();
return;
}
void loadCommonDisplayMode();
void bootstrapHistory();
},
});
@@ -201,6 +236,11 @@ const [GenModal, genModalApi] = useVbenModal({
onConfirm: async () => confirmGenerateAndClose(),
});
/** 与病历面板同一套双击放大编辑 */
const [ExpandModal, expandModalApi] = useVbenModal({
connectedComponent: TextareaExpandModal,
});
function resetState() {
histLoading.value = false;
generating.value = false;
@@ -213,9 +253,30 @@ function resetState() {
chiefComplaint.value = '';
genContext.value = emptyGenContext();
disclaimerText.value = '';
editingKey.value = '';
if (genFieldClickTimer) {
clearTimeout(genFieldClickTimer);
genFieldClickTimer = null;
}
expandModalApi.close();
genModalApi.close();
}
/** 拉取常用词条展示模式,放大弹窗 chips/气泡与病历页一致 */
async function loadCommonDisplayMode() {
try {
const res = await getSystemConfigByKeys(['mr_common_display_mode']);
const mode = String(
res?.mr_common_display_mode ?? res?.data?.mr_common_display_mode ?? 'tags',
) as MrCommonDisplayMode;
if (mode === 'tags' || mode === 'bubble' || mode === 'both') {
commonDisplayMode.value = mode;
}
} catch {
commonDisplayMode.value = 'tags';
}
}
function formatTime(ts: number) {
const n = Number(ts || 0);
if (!n) return '—';
@@ -262,9 +323,93 @@ async function bootstrapHistory() {
}
function openGenerateModal() {
editingKey.value = '';
genModalApi.open();
}
/** 点字段进入行内编辑(与 AI 出方二级弹窗同一交互) */
function startEdit(key: string) {
editingKey.value = key;
}
function fieldValue(key: string) {
if (key === 'chief_complaint') return chiefComplaint.value;
return String(genContext.value?.[key] ?? '');
}
/**
* 生成弹窗字段编辑:主诉写 chiefComplaint其余写 genContext
* 同时 emit 回写病历面板,避免只改弹窗本地态、父页表单仍是旧值
*/
function onFieldInput(key: string, val: string) {
if (key === 'chief_complaint') {
chiefComplaint.value = val;
} else {
genContext.value = {
...genContext.value,
[key]: val,
};
}
emit('sync-medical-record', { [key]: val });
}
function endEdit() {
editingKey.value = '';
}
/**
* 单击延迟进项内编辑:若随后是双击则取消,改为打开放大弹窗
* (否则双击的第一次 click 会先切到 TextArea导致 dblclick 丢事件)
*/
function onGenFieldClick(key: string) {
if (genFieldClickTimer) clearTimeout(genFieldClickTimer);
genFieldClickTimer = setTimeout(() => {
genFieldClickTimer = null;
startEdit(key);
}, 250);
}
function onGenFieldDblClick(key: string, label: string) {
if (genFieldClickTimer) {
clearTimeout(genFieldClickTimer);
genFieldClickTimer = null;
}
openGenFieldExpand(key, label);
}
/**
* 行内 TextArea 用 mousedown.detail===2 捕获双击
* 为什么不用 dblclickblur 会先卸载 TextArea导致 dblclick 丢事件
*/
function onGenTextareaMouseDown(e: MouseEvent, key: string, label: string) {
if (e.detail === 2) {
e.preventDefault();
openGenFieldExpand(key, label);
}
}
/**
* 双击字段:打开与病历页同一套 TextareaExpandModal词条搜索 + 常用)
* joinMode/source 规则对齐 MedicalRecordPanel.openFieldExpand
*/
function openGenFieldExpand(key: string, label: string) {
editingKey.value = '';
const joinMode =
key === 'tongue' || key === 'pulse' ? 'replace' : 'newline';
expandModalApi.setData({
title: label,
value: fieldValue(key) || '',
onConfirm: (v: string) => onFieldInput(key, v),
storeId: storeId.value,
source: 'entry',
fieldCode: key,
showChips: showCommonTags.value,
showCommonInBubble: showCommonInBubble.value,
joinMode,
});
expandModalApi.open();
}
async function onSelectHistory(row: any) {
if (!row?.id || Number(row.id) === PENDING_GEN_ID || row._pending) return;
activeId.value = Number(row.id);
@@ -544,13 +689,13 @@ defineExpose({
</div>
</Drawer>
<!-- 二级 AI 出方一致 Descriptions 双列紧凑表单 -->
<!-- 二级 AI 出方一致 Descriptions 浏览态 + 点击编辑双击同病历页放大 -->
<GenModal>
<div class="space-y-2 text-sm text-foreground">
<AiDisclaimerBanner :text="disclaimerText" />
<div class="text-xs text-muted-foreground">
{{ patientName || '—' }} · {{ sexLabel }} · {{ patientAge || '—' }} ·
主诉必填下方选填有内容会作为 AI 参考若该字段在病历配置中需生成则保留原文
主诉必填单击编辑双击放大同病历页选填有内容会作为 AI 参考
</div>
<div class="max-h-[58vh] overflow-y-auto">
<Descriptions
@@ -559,38 +704,44 @@ defineExpose({
size="small"
class="ai-mr-gen-desc"
>
<Descriptions.Item label="主诉" :span="2">
<Input.TextArea
v-model:value="chiefComplaint"
:rows="3"
placeholder="请确认或补充主诉后再生成"
/>
</Descriptions.Item>
<Descriptions.Item
v-for="f in GEN_OPTIONAL_FIELDS"
:key="f.value"
:label="f.label"
:span="1"
v-for="row in genPreviewRows"
:key="row.key"
:label="row.label"
:span="FULL_SPAN_KEYS.has(row.key) ? 2 : 1"
>
<Input.TextArea
v-model:value="genContext[f.value]"
:rows="2"
:placeholder="`选填${f.label}`"
v-if="editingKey === row.key"
:value="fieldValue(row.key)"
:rows="row.key === 'chief_complaint' ? 3 : 2"
autofocus
:placeholder="`${row.label}(双击放大)`"
@update:value="(v) => onFieldInput(row.key, String(v ?? ''))"
@blur="endEdit"
@mousedown="(e) => onGenTextareaMouseDown(e, row.key, row.label)"
/>
<div
v-else
class="max-h-24 cursor-text overflow-y-auto whitespace-pre-wrap leading-relaxed"
title="单击编辑,双击放大"
@click="onGenFieldClick(row.key)"
@dblclick.stop.prevent="onGenFieldDblClick(row.key, row.label)"
>
{{ fieldValue(row.key) || '(空,单击填写 / 双击放大)' }}
</div>
</Descriptions.Item>
</Descriptions>
</div>
<div class="text-xs text-muted-foreground">
点击开始生成后将关闭本弹窗请在抽屉中等待结果
</div>
</div>
</GenModal>
<ExpandModal />
</template>
<style scoped>
/* label 宽与 AI 出方 .ai-rx-gen-desc 对齐 */
.ai-mr-preview-desc :deep(.ant-descriptions-item-label),
.ai-mr-gen-desc :deep(.ant-descriptions-item-label) {
width: 88px;
width: 110px;
white-space: nowrap;
}
.ai-mr-preview-desc :deep(.ant-descriptions-item-content),

View File

@@ -18,13 +18,21 @@ export async function getDeliveryWarehouseProductInfo(id: number) {
}
/**
* 平台审核status 必为 2 下架或 3 上架;需填 zone_id
* 平台审核(一次提交):
* 主数据审过 + 总仓供货价/售价同步门店 + 绑仓三费 + 可选初始入库
*/
export async function auditDeliveryWarehouseProduct(data: {
id: number;
status: 2 | 3;
zone_id: number;
category_id?: number;
drug_number: string;
market_price: number;
price: number;
quote: number;
promo_fee: number;
platform_fee: number;
initial_stock?: number;
}) {
return requestClient.post<any>(`${prefix}audit`, data);
}

View File

@@ -1,16 +1,24 @@
<script lang="ts" setup>
/**
* 平台仓品审核弹窗
* 必选上架/下架 + 所属分区;审核成功后由列表页打开仓药绑定弹窗
* 平台仓品审核弹窗(单次提交)
* 分区/分类/上下架 + ERP + 总仓供货价/售价 + 绑仓三费 + 可选初始库存
* 提交后由后端一次性完成主数据审过、总仓同步门店、绑仓与入库
*/
import { nextTick, ref } from 'vue';
import { h, onUnmounted, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Descriptions, Image, message, Tag } from 'ant-design-vue';
import { Button, Descriptions, Image, message, Tag } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { getDrugCategoriesTree, getZoneOptions } from '#/views/business/product/drug-categories/api';
import {
calcFeeSum,
isFeeOverSalePrice,
registerFeeOverTipHandler,
resetAutoPromoFeeKey,
syncFeeSumDisplay,
} from '#/views/system/delivery-warehouse-drug/config/form';
import {
auditDeliveryWarehouseProduct,
@@ -19,8 +27,10 @@ import {
const gridApi = ref();
const detail = ref<Record<string, any> | null>(null);
/** 审核成功后回传给列表页,用于打开绑仓弹窗 */
const onBindPreset = ref<((preset: Record<string, any>) => void) | null>(null);
/** 三费超售价红色提示 */
const feeOverTip = ref('');
/** 记录已按售价自动填过平台费的售价,避免反复覆盖手改 */
const lastAutoPlatformSale = ref(0);
const [Form, formApi] = useVbenForm({
wrapperClass: 'grid-cols-12',
@@ -96,12 +106,153 @@ const [Form, formApi] = useVbenForm({
fieldName: 'status',
label: '审核结果',
rules: 'selectRequired',
help: '审核通过后按所选结果上架或下架,并打开绑仓弹窗',
help: '主数据上下架;总仓/绑仓状态同步跟随',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入 ERP 编号无则点右侧「非erp」',
allowClear: true,
},
fieldName: 'drug_number',
label: 'ERP编号',
rules: 'required',
help: '可填真实编号;无 ERP 点「非erp」填入 -1',
// 渲染时再点选,此时 formApi 已就绪
suffix: () =>
h(
Button,
{
size: 'small',
type: 'link',
onClick: (e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
formApi.setFieldValue('drug_number', '-1');
},
},
{ default: () => '非erp' },
),
},
{
component: 'InputNumber',
componentProps: {
placeholder: '请输入仓库供货价',
min: 0,
precision: 4,
class: 'w-full',
},
fieldName: 'market_price',
label: '仓库供货价',
rules: 'required',
help: '写入总仓并同步到门店供货价',
},
{
component: 'InputNumber',
componentProps: {
placeholder: '请输入售价',
min: 0,
precision: 4,
class: 'w-full',
},
fieldName: 'price',
label: '售价',
rules: 'required',
help: '写入总仓销售价;三费合计不得超过该值',
},
{
component: 'InputNumber',
componentProps: {
placeholder: '请输入绑定仓库报价',
min: 0,
precision: 4,
class: 'w-full',
},
fieldName: 'quote',
label: '绑仓报价',
rules: 'required',
help: '默认取仓侧上传报价,可修改',
},
{
component: 'InputNumber',
componentProps: {
placeholder: '请输入平台费',
min: 0,
precision: 4,
class: 'w-full',
},
fieldName: 'platform_fee',
label: '平台费',
rules: 'required',
help: '默认售价的 5%,可改',
},
{
component: 'InputNumber',
componentProps: {
placeholder: '自动=售价−报价−平台费',
min: 0,
precision: 4,
class: 'w-full',
},
fieldName: 'promo_fee',
label: '推广费',
rules: 'required',
help: '有售价时自动计算,可微调',
},
{
component: 'VbenInput',
componentProps: {
disabled: true,
placeholder: '报价+推广费+平台费',
},
fieldName: '_fee_sum',
label: '费用合计',
// 把 price 映射为仓药绑定逻辑里的 sale_price复用三费联动
dependencies: {
async trigger(values, formApiInst) {
const sale = Number(values.price) || 0;
// 售价变化时默认平台费 = 售价 * 5%(仅该售价首次自动填)
if (sale > 0 && lastAutoPlatformSale.value !== sale) {
const platform = Number((sale * 0.05).toFixed(4));
await formApiInst.setFieldValue('platform_fee', platform);
lastAutoPlatformSale.value = sale;
values = { ...values, platform_fee: platform };
}
syncFeeSumDisplay(
{
...values,
sale_price: sale,
},
formApiInst,
);
},
triggerFields: ['quote', 'promo_fee', 'platform_fee', 'price'],
},
},
{
component: 'InputNumber',
componentProps: {
placeholder: '选填,默认 0',
min: 0,
precision: 0,
class: 'w-full',
},
fieldName: 'initial_stock',
label: '初始库存',
help: '大于 0 时绑仓后自动入库并记流水',
defaultValue: 0,
},
],
showDefaultActions: false,
});
registerFeeOverTipHandler((tip) => {
feeOverTip.value = tip;
});
onUnmounted(() => {
registerFeeOverTipHandler(null);
});
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
@@ -121,21 +272,45 @@ const [Modal, modalApi] = useVbenModal({
message.warning('请选择所属分区');
return;
}
const drugNumber = String(values.drug_number || '').trim();
if (!drugNumber) {
message.warning('请填写 ERP 编号(无 ERP 可点「非erp」填 -1');
return;
}
const feeValues = {
quote: values.quote,
promo_fee: values.promo_fee,
platform_fee: values.platform_fee,
sale_price: values.price,
};
if (isFeeOverSalePrice(feeValues)) {
message.warning(
`三费合计已超过售价 ¥${Number(values.price).toFixed(4)}`,
);
return;
}
if (Number(calcFeeSum(feeValues)) <= 0) {
message.warning('报价/推广费/平台费合计必须大于0');
return;
}
modalApi.setState({ loading: true, confirmLoading: true });
auditDeliveryWarehouseProduct({
id: Number(values.id),
status: status as 2 | 3,
zone_id: Number(values.zone_id),
category_id: Number(values.category_id || 0),
drug_number: drugNumber,
market_price: Number(values.market_price),
price: Number(values.price),
quote: Number(values.quote),
promo_fee: Number(values.promo_fee),
platform_fee: Number(values.platform_fee),
initial_stock: Number(values.initial_stock || 0),
})
.then((res: any) => {
message.success('审核成功,完成仓药绑定');
.then(() => {
message.success('审核成功,完成总仓同步与仓药绑定');
gridApi.value?.query?.() ?? gridApi.value?.reload?.();
const preset = res?.bind_preset || res?.data?.bind_preset;
modalApi.close();
if (preset && onBindPreset.value) {
nextTick(() => onBindPreset.value?.(preset));
}
})
.finally(() => {
modalApi.setState({ loading: false, confirmLoading: false });
@@ -145,31 +320,53 @@ const [Modal, modalApi] = useVbenModal({
await formApi.validateAndSubmitForm();
},
async onOpenChange(isOpen: boolean) {
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
onBindPreset.value = isOpen ? modalApi.getData()?.onBindPreset || null : null;
if (!isOpen) {
detail.value = null;
feeOverTip.value = '';
return;
}
await formApi.resetForm();
const data = modalApi.getData<Record<string, any>>() || {};
gridApi.value = data.gridApi ?? null;
resetAutoPromoFeeKey();
lastAutoPlatformSale.value = 0;
feeOverTip.value = '';
await formApi.resetForm();
const id = Number(data.id || data.values?.id || 0);
await formApi.setValues({ id, status: undefined, zone_id: undefined, category_id: undefined });
let row = data.values || { id };
if (id > 0) {
const row = data.values || { id };
try {
const info = await getDeliveryWarehouseProductInfo(id);
detail.value = { ...row, ...(info || {}) };
row = { ...row, ...(info || {}) };
} catch {
detail.value = row;
// 详情失败仍用列表行
}
}
detail.value = row;
const uploadQuote = Number(row.upload_quote || 0);
await formApi.setValues({
id,
status: undefined,
zone_id: undefined,
category_id: undefined,
// 仓侧默认常为 -1保留回填也可点「非erp」重填
drug_number:
row.drug_number != null && String(row.drug_number) !== ''
? String(row.drug_number)
: '',
market_price: undefined,
price: undefined,
quote: uploadQuote > 0 ? uploadQuote : undefined,
platform_fee: undefined,
promo_fee: undefined,
initial_stock: 0,
_fee_sum: uploadQuote > 0 ? String(uploadQuote.toFixed(4)) : '',
});
},
});
</script>
<template>
<Modal title="仓品审核" class="w-[80%] md:w-[50%] lg:w-[40%]">
<Modal title="仓品审核" class="w-[80%] md:w-[560px] lg:w-[520px]">
<div v-if="detail" class="mb-4">
<Descriptions :column="1" size="small" bordered>
<Descriptions.Item label="药品名称">
@@ -196,5 +393,12 @@ const [Modal, modalApi] = useVbenModal({
</Descriptions>
</div>
<Form />
<div
v-if="feeOverTip"
class="mt-2 text-sm"
style="color: hsl(var(--destructive))"
>
{{ feeOverTip }}
</div>
</Modal>
</template>

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
/**
* 平台「仓品审核」列表页
* 审核通过后自动打开仓药绑定弹窗,预填本仓+药品+报价
* 审核弹窗一次提交:主数据 + 总仓同步 + 绑仓三费/入库
*/
import { Page, useVbenModal } from '@vben/common-ui';
@@ -9,7 +9,6 @@ import { Image, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import BindModalDemo from '#/views/system/delivery-warehouse-drug/components/modal.vue';
import AuditModalDemo from './components/audit-modal.vue';
import { formOptions } from './config/search';
@@ -26,37 +25,6 @@ const [AuditModal, auditModalApi] = useVbenModal({
connectedComponent: AuditModalDemo,
});
const [BindModal, bindModalApi] = useVbenModal({
connectedComponent: BindModalDemo,
});
/**
* 审核成功后打开仓药绑定:锁定药品、预填仓库与报价
*/
function openBindFromPreset(preset: Record<string, any>) {
if (!preset?.drug_id || !preset?.warehouse_id) {
return;
}
bindModalApi.setData({
update: false,
lockDrug: true,
presetDrug: preset.presetDrug || {
id: preset.drug_id,
drug_name: preset.drug_name,
specification: preset.specification,
image: preset.image,
},
values: {
warehouse_id: preset.warehouse_id,
drug_id: preset.drug_id,
quote: preset.quote,
status: 2,
},
gridApi,
});
bindModalApi.open();
}
/**
* 打开审核弹窗(仅待审可点)
*/
@@ -68,7 +36,6 @@ function openAudit(row: Record<string, any>) {
id: row.id,
values: row,
gridApi,
onBindPreset: openBindFromPreset,
});
auditModalApi.open();
}
@@ -81,7 +48,6 @@ function auditTagColor(status: number) {
<template>
<Page auto-content-height title="仓品审核">
<AuditModal />
<BindModal />
<Grid>
<template #toolbar-buttons>
<TableAction :actions="[]" :drop-down-actions="[]" />