fix:
1. 优化了审方pc的效果 2. 就诊人信息优化
This commit is contained in:
@@ -18,10 +18,13 @@ import {
|
||||
updateDeliveryWarehouseDrug,
|
||||
} from '#/views/system/delivery-warehouse-drug/api';
|
||||
import {
|
||||
applyAutoPromoFee,
|
||||
calcFeeSum,
|
||||
isFeeOverSalePrice,
|
||||
modalFormProps,
|
||||
registerFeeOverTipHandler,
|
||||
resetAutoPromoFeeKey,
|
||||
seedAutoPromoFeeKey,
|
||||
syncFeeSumDisplay,
|
||||
} from '#/views/system/delivery-warehouse-drug/config/form';
|
||||
import { resolveCentralPrice } from '#/views/system/delivery-warehouse-drug/utils/resolve-central-price';
|
||||
@@ -59,7 +62,8 @@ onUnmounted(() => {
|
||||
});
|
||||
|
||||
/**
|
||||
* 写入售价;新增时默认平台费 = 售价 5%
|
||||
* 写入售价;新增时默认平台费 = 售价 5%,并按差额回填推广费
|
||||
* 编辑仅写售价并锁定依赖 key,不覆盖已有三费
|
||||
*/
|
||||
async function applyCentralPrice(
|
||||
price: number | string | null | undefined,
|
||||
@@ -73,8 +77,18 @@ async function applyCentralPrice(
|
||||
const sale = Number(n.toFixed(4));
|
||||
await formApi.setFieldValue('sale_price', sale);
|
||||
if (isCreate) {
|
||||
await formApi.setFieldValue('platform_fee', Number((sale * 0.05).toFixed(4)));
|
||||
const platform = Number((sale * 0.05).toFixed(4));
|
||||
await formApi.setFieldValue('platform_fee', platform);
|
||||
const values = await formApi.getValues();
|
||||
const merged = { ...values, sale_price: sale, platform_fee: platform };
|
||||
applyAutoPromoFee(merged, formApi);
|
||||
syncFeeSumDisplay(merged, formApi);
|
||||
return;
|
||||
}
|
||||
const values = await formApi.getValues();
|
||||
const merged = { ...values, sale_price: sale };
|
||||
seedAutoPromoFeeKey(merged);
|
||||
syncFeeSumDisplay(merged, formApi);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -148,6 +162,8 @@ const [Modal, modalApi] = useVbenModal({
|
||||
lockDrug.value = !!data.lockDrug && !update;
|
||||
editDrugCard.value = null;
|
||||
feeOverTip.value = '';
|
||||
// 清零自动算费 key,避免沿用上一单的报价/平台费组合
|
||||
resetAutoPromoFeeKey();
|
||||
const salePrice = pickSalePrice(data);
|
||||
// 先重置,再 await 写入,避免异步 setValues 冲掉 sale_price
|
||||
await formApi.resetForm();
|
||||
|
||||
@@ -10,6 +10,18 @@ export function calcFeeSum(values: Record<string, any>) {
|
||||
return (quote + promoFee + platformFee).toFixed(4);
|
||||
}
|
||||
|
||||
/**
|
||||
* 门店推广费 = 售价 − (报价 + 平台费),不低于 0
|
||||
* 有售价时用于自动回填,保证三费合计贴合售价
|
||||
*/
|
||||
export function calcPromoFee(values: Record<string, any>): number {
|
||||
const sale = Number(values.sale_price) || 0;
|
||||
if (!(sale > 0)) return Number(values.promo_fee) || 0;
|
||||
const quote = Number(values.quote) || 0;
|
||||
const platformFee = Number(values.platform_fee) || 0;
|
||||
return Number(Math.max(0, sale - quote - platformFee).toFixed(4));
|
||||
}
|
||||
|
||||
/** 三费是否超过药品售价 */
|
||||
export function isFeeOverSalePrice(values: Record<string, any>) {
|
||||
const sale = Number(values.sale_price);
|
||||
@@ -21,23 +33,74 @@ export function isFeeOverSalePrice(values: Record<string, any>) {
|
||||
type FeeOverTipHandler = (tip: string) => void;
|
||||
let feeOverTipHandler: FeeOverTipHandler | null = null;
|
||||
|
||||
/**
|
||||
* 记录上次自动算费依赖(售价|报价|平台费)
|
||||
* 仅依赖变化时覆盖 promo_fee,避免手改推广费被立刻冲掉
|
||||
*/
|
||||
let lastAutoPromoKey = '';
|
||||
|
||||
/** 注册/注销弹窗内的超售价文本提示处理器 */
|
||||
export function registerFeeOverTipHandler(handler: FeeOverTipHandler | null) {
|
||||
feeOverTipHandler = handler;
|
||||
}
|
||||
|
||||
/** 弹窗打开/重置时清零,避免沿用上一次绑定的 key */
|
||||
export function resetAutoPromoFeeKey() {
|
||||
lastAutoPromoKey = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 费用合计联动:实时重算、三费 max、超售价 help + 文本提示
|
||||
* 为什么放在 trigger:输入任三费即触发,不必等提交
|
||||
* 编辑回填后锁定当前依赖 key,避免一打开就把已有推广费冲掉
|
||||
* 之后只有改报价/平台费/售价才会重新自动算
|
||||
*/
|
||||
export function seedAutoPromoFeeKey(values: Record<string, any>) {
|
||||
const sale = Number(values.sale_price) || 0;
|
||||
if (!(sale > 0)) {
|
||||
lastAutoPromoKey = '';
|
||||
return;
|
||||
}
|
||||
lastAutoPromoKey = `${sale}|${Number(values.quote) || 0}|${Number(values.platform_fee) || 0}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 有售价且 quote/platform/sale 变化时,自动回填门店推广费
|
||||
* @returns 回填后的 values(含最新 promo_fee),供合计展示使用
|
||||
*/
|
||||
export function applyAutoPromoFee(
|
||||
values: Record<string, any>,
|
||||
formApi: any,
|
||||
): Record<string, any> {
|
||||
const sale = Number(values.sale_price) || 0;
|
||||
if (!(sale > 0)) {
|
||||
return values;
|
||||
}
|
||||
const key = `${sale}|${Number(values.quote) || 0}|${Number(values.platform_fee) || 0}`;
|
||||
const nextPromo = calcPromoFee(values);
|
||||
// 依赖未变:可能是手改推广费触发,不覆盖
|
||||
if (key === lastAutoPromoKey) {
|
||||
return values;
|
||||
}
|
||||
lastAutoPromoKey = key;
|
||||
const cur = Number(values.promo_fee);
|
||||
if (!(Math.abs(cur - nextPromo) < 0.00005)) {
|
||||
formApi.setFieldValue('promo_fee', nextPromo);
|
||||
}
|
||||
return { ...values, promo_fee: nextPromo };
|
||||
}
|
||||
|
||||
/**
|
||||
* 费用合计联动:先按差额回填推广费,再刷新合计 / 超售价提示
|
||||
* 为什么放在 trigger:输入报价或平台费即触发,不必等提交
|
||||
* @returns 超售价提示文案(无则空串)
|
||||
*/
|
||||
export function syncFeeSumDisplay(
|
||||
values: Record<string, any>,
|
||||
formApi: any,
|
||||
): string {
|
||||
const sum = calcFeeSum(values);
|
||||
const merged = applyAutoPromoFee(values, formApi);
|
||||
const sum = calcFeeSum(merged);
|
||||
formApi.setFieldValue('_fee_sum', sum);
|
||||
const sale = Number(values.sale_price) || 0;
|
||||
const sale = Number(merged.sale_price) || 0;
|
||||
const over = sale > 0 && Number(sum) > sale;
|
||||
const tip = over
|
||||
? `三费合计已超过药品售价 ¥${sale.toFixed(4)},请下调报价/推广费/平台费`
|
||||
@@ -45,7 +108,7 @@ export function syncFeeSumDisplay(
|
||||
const feeHelp = over
|
||||
? tip
|
||||
: sale > 0
|
||||
? `报价+推广费+平台费之和不得超过药品售价 ¥${sale.toFixed(4)}`
|
||||
? `推广费自动=售价−(报价+平台费);合计不得超过 ¥${sale.toFixed(4)}`
|
||||
: '报价+推广费+平台费之和,保存时需大于0';
|
||||
// 单项输入上限:有售价时不得超过售价;同时刷新合计 help
|
||||
const feeInputProps = {
|
||||
@@ -64,9 +127,10 @@ export function syncFeeSumDisplay(
|
||||
},
|
||||
{
|
||||
fieldName: 'promo_fee',
|
||||
help: sale > 0 ? '自动计算:售价 − (报价 + 平台费),可微调' : undefined,
|
||||
componentProps: {
|
||||
...feeInputProps,
|
||||
placeholder: '请输入推广费',
|
||||
placeholder: sale > 0 ? '自动=售价−报价−平台费' : '请输入推广费',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -174,6 +238,7 @@ export const modalFormProps: VbenFormProps = {
|
||||
},
|
||||
fieldName: 'promo_fee',
|
||||
label: '推广费',
|
||||
help: '有售价时自动=售价−(报价+平台费)',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
@@ -204,7 +269,26 @@ export const modalFormProps: VbenFormProps = {
|
||||
triggerFields: ['quote', 'promo_fee', 'platform_fee', 'sale_price'],
|
||||
},
|
||||
},
|
||||
// 绑定创建时库存固定为 0,后续在「库存管理」入库/出库,禁止表单直改数量
|
||||
// 创建时可填初始库存,提交后走入库流水(编辑时不展示)
|
||||
{
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '选填,默认 0',
|
||||
min: 0,
|
||||
precision: 0,
|
||||
class: 'w-full',
|
||||
},
|
||||
fieldName: 'initial_stock',
|
||||
label: '初始库存',
|
||||
help: '大于 0 时绑定成功后自动入库并记流水',
|
||||
defaultValue: 0,
|
||||
dependencies: {
|
||||
show(values) {
|
||||
return !values?.id;
|
||||
},
|
||||
triggerFields: ['id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 出入库记录抽屉:卡片列表展示
|
||||
* 含手动入库/出库、绑定初始入库及操作人
|
||||
*/
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
|
||||
import { Empty, Spin, Table, Tag } from 'ant-design-vue';
|
||||
import { Empty, Pagination, Spin, Tag } from 'ant-design-vue';
|
||||
|
||||
import { getDeliveryWarehouseStockLogList } from '#/views/system/delivery-warehouse-stock/api';
|
||||
|
||||
@@ -19,31 +23,27 @@ const pager = reactive({
|
||||
});
|
||||
const filter = reactive({
|
||||
warehouse_drug_id: 0 as number,
|
||||
drug_id: 0 as number,
|
||||
});
|
||||
|
||||
const columns = [
|
||||
{ title: '时间', dataIndex: 'created_at', key: 'created_at', width: 170 },
|
||||
{ title: '类型', dataIndex: 'biz_type_txt', key: 'biz_type_txt', width: 120 },
|
||||
{ title: '数量', dataIndex: 'qty', key: 'qty', width: 80 },
|
||||
{ title: '库存前→后', key: 'stock_chg', width: 120 },
|
||||
{ title: '冻结前→后', key: 'frozen_chg', width: 120 },
|
||||
{ title: '订单ID', dataIndex: 'order_id', key: 'order_id', width: 90 },
|
||||
{ title: '备注', dataIndex: 'remark', key: 'remark' },
|
||||
];
|
||||
/** 类型 Tag 颜色:入绿 / 出橙 / 其它默认 */
|
||||
function bizTagColor(bizType: string) {
|
||||
if (bizType === 'manual_in' || bizType === 'refund_in') return 'success';
|
||||
if (bizType === 'manual_out' || bizType === 'ship_out') return 'warning';
|
||||
return 'default';
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
if (!filter.warehouse_drug_id && !filter.drug_id) {
|
||||
if (!filter.warehouse_drug_id) {
|
||||
rows.value = [];
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
// 只传 warehouse_drug_id,避免与 drug_id AND 干扰
|
||||
const res = await getDeliveryWarehouseStockLogList({
|
||||
page: pager.current,
|
||||
pageSize: pager.pageSize,
|
||||
warehouse_drug_id: filter.warehouse_drug_id || undefined,
|
||||
drug_id: filter.drug_id || undefined,
|
||||
warehouse_drug_id: filter.warehouse_drug_id,
|
||||
});
|
||||
rows.value = res?.items ?? res?.list ?? [];
|
||||
pager.total = Number(res?.total ?? 0);
|
||||
@@ -63,7 +63,6 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
||||
const row = data?.row || {};
|
||||
drugName.value = row.drug?.drug_name || '';
|
||||
filter.warehouse_drug_id = Number(row.id || 0);
|
||||
filter.drug_id = Number(row.drug_id || 0);
|
||||
pager.current = 1;
|
||||
void loadLogs();
|
||||
},
|
||||
@@ -73,38 +72,100 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
||||
<template>
|
||||
<Drawer :title="`出入库记录${drugName ? ` - ${drugName}` : ''}`">
|
||||
<Spin :spinning="loading">
|
||||
<Table
|
||||
v-if="rows.length"
|
||||
:columns="columns"
|
||||
:data-source="rows"
|
||||
:pagination="{
|
||||
current: pager.current,
|
||||
pageSize: pager.pageSize,
|
||||
total: pager.total,
|
||||
onChange: (p: number) => {
|
||||
pager.current = p;
|
||||
void loadLogs();
|
||||
},
|
||||
}"
|
||||
row-key="id"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'biz_type_txt'">
|
||||
<Tag>{{ record.biz_type_txt || record.biz_type }}</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'stock_chg'">
|
||||
{{ record.stock_before }} → {{ record.stock_after }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'frozen_chg'">
|
||||
{{ record.frozen_before }} → {{ record.frozen_after }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'order_id'">
|
||||
{{ record.order_id > 0 ? record.order_id : '-' }}
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
<div v-if="rows.length" class="log-card-list">
|
||||
<div
|
||||
v-for="record in rows"
|
||||
:key="record.id"
|
||||
class="log-card border-border bg-card"
|
||||
>
|
||||
<div class="log-card__head">
|
||||
<Tag :color="bizTagColor(record.biz_type)">
|
||||
{{ record.biz_type_txt || record.biz_type }}
|
||||
</Tag>
|
||||
<span class="text-muted-foreground text-xs">{{
|
||||
record.created_at
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="log-card__body">
|
||||
<div class="log-card__row">
|
||||
<span class="text-muted-foreground">数量</span>
|
||||
<span class="text-foreground font-medium">{{ record.qty }}</span>
|
||||
</div>
|
||||
<div class="log-card__row">
|
||||
<span class="text-muted-foreground">库存</span>
|
||||
<span class="text-foreground"
|
||||
>{{ record.stock_before }} → {{ record.stock_after }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="log-card__row">
|
||||
<span class="text-muted-foreground">冻结</span>
|
||||
<span class="text-foreground"
|
||||
>{{ record.frozen_before }} → {{ record.frozen_after }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="log-card__row">
|
||||
<span class="text-muted-foreground">操作人</span>
|
||||
<span class="text-foreground">{{
|
||||
record.operator_name || '系统'
|
||||
}}</span>
|
||||
</div>
|
||||
<div v-if="Number(record.order_id) > 0" class="log-card__row">
|
||||
<span class="text-muted-foreground">订单ID</span>
|
||||
<span class="text-foreground">{{ record.order_id }}</span>
|
||||
</div>
|
||||
<div v-if="record.remark" class="log-card__row">
|
||||
<span class="text-muted-foreground">备注</span>
|
||||
<span class="text-foreground">{{ record.remark }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end">
|
||||
<Pagination
|
||||
v-model:current="pager.current"
|
||||
:page-size="pager.pageSize"
|
||||
:total="pager.total"
|
||||
size="small"
|
||||
@change="
|
||||
(p: number) => {
|
||||
pager.current = p;
|
||||
void loadLogs();
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Empty v-else class="py-10" description="暂无出入库记录" />
|
||||
</Spin>
|
||||
</Drawer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.log-card-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.log-card {
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
background: hsl(var(--card));
|
||||
}
|
||||
.log-card__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.log-card__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.log-card__row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user