1. 配送仓库
This commit is contained in:
@@ -172,6 +172,8 @@ const searchDrugs = debounce(async (keyword: string) => {
|
||||
_specification: drug.specification || item.specification || '',
|
||||
_supplier: drug.supplier?.name || item.supplier?.name || '',
|
||||
_price: Number(item.price ?? 0),
|
||||
// 已绑定配送仓:列表展示「多仓」标识
|
||||
_hasDeliveryWarehouse: Number(item.has_delivery_warehouse ?? 0) === 1,
|
||||
_timeId: drug.time_id || 0,
|
||||
_typeId: drug.type_id || 0,
|
||||
_frequencyId: drug.frequency_id || 0,
|
||||
@@ -423,9 +425,12 @@ watch(
|
||||
<div v-else class="drug-item__img-placeholder">无图</div>
|
||||
</div>
|
||||
|
||||
<!-- 药品信息:药名 / ID / 规格 / 供应商 -->
|
||||
<!-- 药品信息:药名 / 多仓 / ID / 规格 / 供应商 -->
|
||||
<div class="drug-item__info">
|
||||
<div class="drug-item__name">{{ item._drugName }}</div>
|
||||
<div class="drug-item__name-row">
|
||||
<div class="drug-item__name">{{ item._drugName }}</div>
|
||||
<span v-if="item._hasDeliveryWarehouse" class="drug-item__multi-wh">多仓</span>
|
||||
</div>
|
||||
<div class="drug-item__meta">
|
||||
<span class="drug-item__id">ID:{{ item._drugId }}</span>
|
||||
<span v-if="item._specification" class="drug-item__spec">
|
||||
@@ -565,6 +570,14 @@ watch(
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
@@ -572,7 +585,20 @@ watch(
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 配送仓绑定药品标识:小号胶囊,不抢主信息 */
|
||||
&__multi-wh {
|
||||
flex-shrink: 0;
|
||||
padding: 0 6px;
|
||||
height: 18px;
|
||||
line-height: 16px;
|
||||
font-size: 11px;
|
||||
color: #2b6de5;
|
||||
background: rgba(43, 109, 229, 0.08);
|
||||
border: 1px solid rgba(43, 109, 229, 0.45);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
|
||||
@@ -571,6 +571,8 @@ export const usePrescriptionStore = defineStore('prescription', () => {
|
||||
frequency_id: data.drug.frequency_id,
|
||||
unit_id: data.drug.unit_id,
|
||||
image: data.drug.image,
|
||||
// 规格(选仓弹窗等处展示)
|
||||
specification: data.drug?.specification || data.specification || '',
|
||||
instruction: data.drug.instruction,
|
||||
type: data.drug.type,
|
||||
select_number: (() => {
|
||||
@@ -701,6 +703,8 @@ export const usePrescriptionStore = defineStore('prescription', () => {
|
||||
frequency_id: data.drug.frequency_id,
|
||||
unit_id: data.drug.unit_id,
|
||||
image: data.drug.image,
|
||||
// 规格(选仓弹窗等处展示)
|
||||
specification: data.drug?.specification || data.specification || '',
|
||||
instruction: data.drug.instruction,
|
||||
type: data.drug.type,
|
||||
};
|
||||
@@ -987,8 +991,13 @@ export const usePrescriptionStore = defineStore('prescription', () => {
|
||||
// 诊所选择参数(转诊挂号时自动使用承接方诊所ID)
|
||||
send_mode: finalSendMode,
|
||||
custom_store_id: finalSendMode === 1 ? finalStoreId : null,
|
||||
// 药店按药选配送仓映射 drug_id -> warehouse_id
|
||||
warehouse_map: warehouseMap || undefined,
|
||||
// 在线复诊手选仓:列表格式避免数字键对象序列化丢失
|
||||
warehouse_map: warehouseMap
|
||||
? Object.entries(warehouseMap).map(([drug_id, warehouse_id]) => ({
|
||||
drug_id: Number(drug_id),
|
||||
warehouse_id: Number(warehouse_id),
|
||||
}))
|
||||
: undefined,
|
||||
});
|
||||
|
||||
const res = await response;
|
||||
@@ -1019,7 +1028,9 @@ export const usePrescriptionStore = defineStore('prescription', () => {
|
||||
userStore.currentUser.doctor_id,
|
||||
);
|
||||
resetForm();
|
||||
|
||||
// 发送成功后清空本挂号下全部分类草稿(含另一分类与 activeCategory)
|
||||
clearLocalPrescriptionCache();
|
||||
|
||||
// 返回包含转诊信息的响应数据
|
||||
return {
|
||||
success: true,
|
||||
@@ -1034,6 +1045,33 @@ export const usePrescriptionStore = defineStore('prescription', () => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除当前挂号会话的处方 localStorage 草稿
|
||||
* 原因:resetForm 只会把当前分类写成空数组,另一分类与 activeCategory 会残留,再次打开仍会回填
|
||||
*/
|
||||
const clearLocalPrescriptionCache = () => {
|
||||
const registerId = currentRegisterId.value;
|
||||
if (!registerId) return;
|
||||
try {
|
||||
// 与小程序 PrescriptionStorage.clearAllPrescriptionData 对齐的分类集合
|
||||
const categories = [1, 2, 3, 5, 6, 7];
|
||||
categories.forEach((cat) => {
|
||||
localStorage.removeItem(
|
||||
`${storagePrefix.value}prescriptionData_${cat}_${registerId}`,
|
||||
);
|
||||
// 清理历史双横线孤儿 key(旧版弹窗 `${prefix}-prescriptionData_`)
|
||||
localStorage.removeItem(
|
||||
`${storagePrefix.value}-prescriptionData_${cat}_${registerId}`,
|
||||
);
|
||||
});
|
||||
localStorage.removeItem(
|
||||
`${storagePrefix.value}activeCategory${registerId}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('清除处方本地缓存失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
updateCurrentDrugs([]);
|
||||
diagnosis.value = '';
|
||||
@@ -1168,6 +1206,7 @@ export const usePrescriptionStore = defineStore('prescription', () => {
|
||||
checkChineseMedicineConflict,
|
||||
sendPrescription,
|
||||
resetForm,
|
||||
clearLocalPrescriptionCache,
|
||||
changeCategory,
|
||||
getProcessRuleListData,
|
||||
splitString,
|
||||
|
||||
15
apps/web-antd/src/utils/formatStoreNameWithHu.ts
Normal file
15
apps/web-antd/src/utils/formatStoreNameWithHu.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* 在线处方诊所名追加「(互)」:is_online 为 1/2/3(问诊/复诊等),线下 0 不加
|
||||
*/
|
||||
export function formatStoreNameWithHu(
|
||||
storeName: string | null | undefined,
|
||||
isOnline: number | string | null | undefined,
|
||||
): string {
|
||||
const name = String(storeName ?? '').trim();
|
||||
if (!name) return '';
|
||||
const online = Number(isOnline);
|
||||
if (online === 1 || online === 2 || online === 3) {
|
||||
return name.endsWith('(互)') ? name : `${name}(互)`;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
@@ -224,50 +224,103 @@ const [WarehouseSelectModals, warehouseSelectModalApi] = useVbenModal({
|
||||
});
|
||||
|
||||
/**
|
||||
* 药店在线问诊:若药品有配送仓绑定,则先弹选仓再发送
|
||||
* 在线复诊发送前:有仓药绑定时弹卡片选仓(默认最低价)
|
||||
* @returns true=已拦截发送等待选仓;false=无需选仓可继续发送
|
||||
*/
|
||||
const tryPharmacyWarehouseSelect = async (
|
||||
doctorSecondSignValue: number,
|
||||
sendMode: number,
|
||||
customStoreId: number | null,
|
||||
): Promise<boolean> => {
|
||||
const storagePrefix =
|
||||
modalData.value?.storagePrefix || 'onlineConsultation-';
|
||||
// 诊所在线复诊前缀不走手动选仓(后端自动按报价)
|
||||
if (storagePrefix === 'onlineConsultationClinic-') {
|
||||
return false;
|
||||
}
|
||||
const drugs = prescriptionStore.currentDrugs || [];
|
||||
const drugIds = drugs.map((d: any) => Number(d.id)).filter((id: number) => id > 0);
|
||||
const drugIds: number[] = [];
|
||||
const needQtyMap: Record<number, number> = {};
|
||||
for (const d of drugs) {
|
||||
const id = Number(d?.id ?? 0);
|
||||
const qty = Number(d?.select_number ?? d?.number ?? 0);
|
||||
if (id <= 0 || qty <= 0) {
|
||||
continue;
|
||||
}
|
||||
drugIds.push(id);
|
||||
needQtyMap[id] = (needQtyMap[id] || 0) + qty;
|
||||
}
|
||||
if (drugIds.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const needQtyMap: Record<number, number> = {};
|
||||
drugs.forEach((d: any) => {
|
||||
needQtyMap[Number(d.id)] = Number(d.select_number || d.number || 1);
|
||||
});
|
||||
const optionsMap = await getDeliveryWarehouseOptionsByDrugs({
|
||||
drug_ids: drugIds.join(','),
|
||||
need_qty_map: needQtyMap,
|
||||
});
|
||||
const rows: any[] = [];
|
||||
Object.keys(optionsMap || {}).forEach((drugId) => {
|
||||
const list = optionsMap[drugId] || [];
|
||||
if (!list.length) return;
|
||||
const drug = drugs.find((d: any) => Number(d.id) === Number(drugId));
|
||||
rows.push({
|
||||
drug_id: Number(drugId),
|
||||
drug_name: drug?.drug_name || drug?.name || `药品#${drugId}`,
|
||||
options: list,
|
||||
try {
|
||||
const res = await getDeliveryWarehouseOptionsByDrugs({
|
||||
drug_ids: drugIds,
|
||||
need_qty_map: needQtyMap,
|
||||
});
|
||||
});
|
||||
if (rows.length === 0) {
|
||||
return false;
|
||||
const map = (res?.result ?? res ?? {}) as Record<string, any[]>;
|
||||
const rows: Array<{
|
||||
drug_id: number;
|
||||
drug_name: string;
|
||||
options: Array<{
|
||||
warehouse_id: number;
|
||||
warehouse_name: string;
|
||||
quote: string;
|
||||
available_stock: number;
|
||||
}>;
|
||||
}> = [];
|
||||
for (const d of drugs) {
|
||||
const id = Number(d?.id ?? 0);
|
||||
if (id <= 0) {
|
||||
continue;
|
||||
}
|
||||
const options = map[String(id)] || map[id] || [];
|
||||
if (!Array.isArray(options) || options.length === 0) {
|
||||
continue;
|
||||
}
|
||||
rows.push({
|
||||
drug_id: id,
|
||||
// 优先用仓选项接口带回的药品信息,避免处方草稿未存规格/图
|
||||
drug_name: String(
|
||||
options[0]?.drug_name ||
|
||||
d?.drug_name ||
|
||||
d?.name ||
|
||||
`药品#${id}`,
|
||||
),
|
||||
image: String(
|
||||
options[0]?.image ||
|
||||
d?.image ||
|
||||
d?._image ||
|
||||
d?.drug?.image ||
|
||||
'',
|
||||
),
|
||||
specification: String(
|
||||
options[0]?.specification ||
|
||||
d?.specification ||
|
||||
d?.drug?.specification ||
|
||||
'',
|
||||
),
|
||||
options: options.map((o) => ({
|
||||
warehouse_id: Number(o.warehouse_id),
|
||||
warehouse_name: String(o.warehouse_name || ''),
|
||||
quote: String(o.quote ?? '0'),
|
||||
available_stock: Number(o.available_stock ?? 0),
|
||||
// 保留药品展示字段,供弹窗从 options[0] 回退读取
|
||||
drug_name: String(o.drug_name || ''),
|
||||
image: String(o.image || ''),
|
||||
specification: String(o.specification || ''),
|
||||
})),
|
||||
});
|
||||
}
|
||||
if (rows.length === 0) {
|
||||
return false;
|
||||
}
|
||||
pendingSendParams.value = {
|
||||
doctorSecondSignValue,
|
||||
sendMode,
|
||||
customStoreId,
|
||||
};
|
||||
warehouseSelectModalApi.setData({ rows });
|
||||
warehouseSelectModalApi.open();
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || '加载配送仓库失败');
|
||||
return true;
|
||||
}
|
||||
pendingSendParams.value = { doctorSecondSignValue, sendMode, customStoreId };
|
||||
warehouseSelectModalApi.setData({ rows });
|
||||
warehouseSelectModalApi.open();
|
||||
return true;
|
||||
};
|
||||
|
||||
const onWarehouseSelected = async (warehouseMap: Record<number, number>) => {
|
||||
@@ -301,6 +354,7 @@ const handleSendPrescription = async (doctorSecondSignValue = 0) => {
|
||||
sendMode = 1;
|
||||
customStoreId = storeInfo.store_id;
|
||||
}
|
||||
// 有绑仓药品时先卡片选仓,再发送
|
||||
const needSelect = await tryPharmacyWarehouseSelect(
|
||||
doctorSecondSignValue,
|
||||
sendMode,
|
||||
@@ -552,6 +606,7 @@ async function handleSelectCommonPrescription(data: any, type: number) {
|
||||
frequency_id: recipe.frequency_id,
|
||||
unit_id: recipe.unit_id,
|
||||
image: recipe.image,
|
||||
specification: recipe.specification || recipe.drug?.specification || '',
|
||||
instruction: recipe.instruction,
|
||||
type: recipe.type,
|
||||
select_number: recipe.select_number || 1,
|
||||
@@ -575,6 +630,7 @@ async function handleSelectCommonPrescription(data: any, type: number) {
|
||||
price: recipe.price || 0,
|
||||
buy_price: recipe.buy_price,
|
||||
way_id: recipe.way_id || 0,
|
||||
specification: recipe.specification || recipe.drug?.specification || '',
|
||||
select_number: 1,
|
||||
};
|
||||
// 检查是否已存在
|
||||
@@ -666,6 +722,12 @@ function handleSimpleProductSelect(drug: any) {
|
||||
number: 1,
|
||||
price: drug._price || drug.price,
|
||||
image: drug._image || drug.drug?.image || drug.image,
|
||||
// 规格(选仓弹窗等处展示)
|
||||
specification:
|
||||
drug._specification ||
|
||||
drug.drug?.specification ||
|
||||
drug.specification ||
|
||||
'',
|
||||
instruction: drug.drug?.instruction || drug.instruction || '',
|
||||
type: drug.drug?.type || drug.type || prescriptionStore.activeCategory,
|
||||
select_number: 1,
|
||||
|
||||
@@ -1,92 +1,210 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 药店开方:按药品选择配送仓库
|
||||
* 交互尽量简单:每个药品一个下拉,确认后回传 warehouse_map
|
||||
* 在线复诊开方:按药品分步卡片选配送仓库
|
||||
* 默认选中 options[0](与后端 auto 一致:quote ASC 最低价)
|
||||
* 样式使用主题 token,适配亮/暗色
|
||||
*/
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message, Select } from 'ant-design-vue';
|
||||
import { Button, Image, message } from 'ant-design-vue';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'confirm', map: Record<number, number>): void;
|
||||
}>();
|
||||
|
||||
const drugRows = ref<
|
||||
Array<{
|
||||
drug_id: number;
|
||||
drug_name: string;
|
||||
options: Array<{
|
||||
warehouse_id: number;
|
||||
warehouse_name: string;
|
||||
quote: string;
|
||||
available_stock: number;
|
||||
}>;
|
||||
warehouse_id?: number;
|
||||
}>
|
||||
>([]);
|
||||
type WarehouseOption = {
|
||||
warehouse_id: number;
|
||||
warehouse_name: string;
|
||||
quote: string;
|
||||
available_stock: number;
|
||||
/** 接口可能在 option 上带药品展示字段,供 row 回退 */
|
||||
drug_name?: string;
|
||||
image?: string;
|
||||
specification?: string;
|
||||
};
|
||||
|
||||
type DrugRow = {
|
||||
drug_id: number;
|
||||
drug_name: string;
|
||||
image?: string;
|
||||
specification?: string;
|
||||
options: WarehouseOption[];
|
||||
warehouse_id?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* 从 row 或 options[0] 取药品展示字段(接口规格在 option 上)
|
||||
*/
|
||||
function resolveDrugMeta(row: DrugRow) {
|
||||
const first = row.options?.[0];
|
||||
return {
|
||||
drug_name: row.drug_name || first?.drug_name || '',
|
||||
image: row.image || first?.image || '',
|
||||
specification: row.specification || first?.specification || '',
|
||||
};
|
||||
}
|
||||
|
||||
const drugRows = ref<DrugRow[]>([]);
|
||||
/** 当前步骤下标(0-based) */
|
||||
const stepIndex = ref(0);
|
||||
|
||||
const totalSteps = computed(() => drugRows.value.length);
|
||||
const currentRow = computed(() => drugRows.value[stepIndex.value] || null);
|
||||
/** 当前步药品展示:名/图/规格,含 option 回退 */
|
||||
const currentDrugMeta = computed(() =>
|
||||
currentRow.value
|
||||
? resolveDrugMeta(currentRow.value)
|
||||
: { drug_name: '', image: '', specification: '' },
|
||||
);
|
||||
const isFirstStep = computed(() => stepIndex.value <= 0);
|
||||
const isLastStep = computed(
|
||||
() => stepIndex.value >= Math.max(totalSteps.value - 1, 0),
|
||||
);
|
||||
const canGoNext = computed(() => !!currentRow.value?.warehouse_id);
|
||||
|
||||
/** 点击卡片选中当前步骤药品的配送仓 */
|
||||
function selectWarehouse(warehouseId: number) {
|
||||
const row = currentRow.value;
|
||||
if (!row) return;
|
||||
row.warehouse_id = warehouseId;
|
||||
}
|
||||
|
||||
function goPrev() {
|
||||
if (!isFirstStep.value) {
|
||||
stepIndex.value -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
function goNext() {
|
||||
if (!canGoNext.value) {
|
||||
message.error('请先选择配送仓库');
|
||||
return;
|
||||
}
|
||||
if (!isLastStep.value) {
|
||||
stepIndex.value += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/** 汇总 warehouse_map 并确认 */
|
||||
function confirmAll() {
|
||||
if (!canGoNext.value) {
|
||||
message.error('请先选择配送仓库');
|
||||
return;
|
||||
}
|
||||
const map: Record<number, number> = {};
|
||||
for (const row of drugRows.value) {
|
||||
if (!row.warehouse_id) {
|
||||
message.error(`请为【${row.drug_name}】选择配送仓库`);
|
||||
return;
|
||||
}
|
||||
map[row.drug_id] = row.warehouse_id;
|
||||
}
|
||||
emit('confirm', map);
|
||||
modalApi.close();
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
footer: false,
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm() {
|
||||
const map: Record<number, number> = {};
|
||||
for (const row of drugRows.value) {
|
||||
if (!row.warehouse_id) {
|
||||
message.error(`请为【${row.drug_name}】选择配送仓库`);
|
||||
return;
|
||||
}
|
||||
map[row.drug_id] = row.warehouse_id;
|
||||
}
|
||||
emit('confirm', map);
|
||||
modalApi.close();
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
if (isOpen) {
|
||||
const data = modalApi.getData<{ rows: typeof drugRows.value }>();
|
||||
drugRows.value = (data?.rows || []).map((row) => ({
|
||||
...row,
|
||||
warehouse_id: row.options?.[0]?.warehouse_id,
|
||||
}));
|
||||
const data = modalApi.getData<{ rows: DrugRow[] }>();
|
||||
// 默认选中每药 options[0](最低 quote);规格/图/名可从 option 回退
|
||||
drugRows.value = (data?.rows || []).map((row) => {
|
||||
const meta = resolveDrugMeta(row);
|
||||
return {
|
||||
...row,
|
||||
warehouse_id: row.options?.[0]?.warehouse_id,
|
||||
drug_name: meta.drug_name || `药品#${row.drug_id}`,
|
||||
image: meta.image,
|
||||
specification: meta.specification,
|
||||
};
|
||||
});
|
||||
stepIndex.value = 0;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const canConfirm = computed(() =>
|
||||
drugRows.value.every((row) => !!row.warehouse_id),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="选择配送仓库" class="w-[520px]" :confirm-disabled="!canConfirm">
|
||||
<div class="space-y-3 py-2">
|
||||
<Modal title="选择配送仓库" class="w-[640px]">
|
||||
<div v-if="currentRow" class="space-y-4 py-2">
|
||||
<div class="text-muted-foreground text-sm">
|
||||
第 {{ stepIndex + 1 }} / {{ totalSteps }} 个药品
|
||||
</div>
|
||||
<!-- 当前药品信息:图 / 名 / 规格 -->
|
||||
<div
|
||||
v-for="row in drugRows"
|
||||
:key="row.drug_id"
|
||||
class="flex items-center gap-3"
|
||||
class="border-border bg-muted/30 flex items-start gap-3 rounded-lg border p-3"
|
||||
>
|
||||
<div class="w-40 shrink-0 truncate" :title="row.drug_name">
|
||||
{{ row.drug_name }}
|
||||
</div>
|
||||
<Select
|
||||
v-model:value="row.warehouse_id"
|
||||
class="flex-1"
|
||||
placeholder="请选择配送仓库"
|
||||
:options="
|
||||
row.options.map((o) => ({
|
||||
label: `${o.warehouse_name}(报价${o.quote},库存${o.available_stock})`,
|
||||
value: o.warehouse_id,
|
||||
}))
|
||||
"
|
||||
<Image
|
||||
v-if="currentDrugMeta.image"
|
||||
:src="currentDrugMeta.image"
|
||||
:width="64"
|
||||
:height="64"
|
||||
class="shrink-0 rounded object-cover"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="bg-accent text-muted-foreground flex h-16 w-16 shrink-0 items-center justify-center rounded text-xs"
|
||||
>
|
||||
暂无图
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div
|
||||
class="text-foreground truncate text-base font-medium"
|
||||
:title="currentDrugMeta.drug_name"
|
||||
>
|
||||
{{ currentDrugMeta.drug_name }}
|
||||
</div>
|
||||
<div class="text-muted-foreground mt-1 text-sm">
|
||||
规格:{{ currentDrugMeta.specification || '--' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="drugRows.length === 0" class="text-gray-400">
|
||||
当前药品无需选择配送仓库
|
||||
<!-- 仓卡片 -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="opt in currentRow.options"
|
||||
:key="opt.warehouse_id"
|
||||
type="button"
|
||||
class="min-w-[140px] rounded-lg border px-3 py-2 text-left transition-colors"
|
||||
:class="
|
||||
currentRow.warehouse_id === opt.warehouse_id
|
||||
? 'border-primary bg-primary/10 ring-primary ring-1'
|
||||
: 'border-border hover:border-primary/50'
|
||||
"
|
||||
@click="selectWarehouse(opt.warehouse_id)"
|
||||
>
|
||||
<div class="text-foreground text-sm font-medium">
|
||||
{{ opt.warehouse_name }}
|
||||
</div>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
供货价 {{ opt.quote }} · 库存 {{ opt.available_stock }}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-muted-foreground py-6">当前药品无需选择配送仓库</div>
|
||||
<!-- 自定义底栏:上一步 / 下一步 / 确认 -->
|
||||
<div class="border-border mt-4 flex justify-end gap-2 border-t pt-3">
|
||||
<Button v-if="!isFirstStep" @click="goPrev">上一步</Button>
|
||||
<Button
|
||||
v-if="!isLastStep"
|
||||
type="primary"
|
||||
:disabled="!canGoNext"
|
||||
@click="goNext"
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
<Button v-else type="primary" :disabled="!canGoNext" @click="confirmAll">
|
||||
确认
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -79,6 +79,26 @@ export async function expressDetailByOrderId(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`express-detail/detail-by-order`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 超管:将历史订单级运单同步到分包裹 shipment
|
||||
*/
|
||||
export async function syncLegacyShipmentApi(orderId: number) {
|
||||
return requestClient.post<any>(`${prefix}sync-legacy-shipment`, {
|
||||
order_id: orderId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单页改仓:将 from 仓名下未出库明细切到 to 仓(0=萧康医药本仓库)
|
||||
*/
|
||||
export async function changeDeliveryWarehouseApi(data: {
|
||||
order_id: number;
|
||||
from_warehouse_id: number;
|
||||
to_warehouse_id: number;
|
||||
}) {
|
||||
return requestClient.post<any>(`${prefix}change-delivery-warehouse`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出订单数据(后端 Excel,旧接口保留)
|
||||
*/
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer, useVbenModal } from '@vben/common-ui';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
|
||||
import { Button, Card, Descriptions, Image, Space, Tag, Timeline } from 'ant-design-vue';
|
||||
import { Button, Card, Descriptions, Image, Space, Tabs, Tag, message } from 'ant-design-vue';
|
||||
|
||||
import OrderTraceDrawer from '#/views/business/order/components/order-trace-drawer.vue';
|
||||
|
||||
@@ -11,15 +12,16 @@ import OrderPricePercentAdjustDrawer from '#/views/business/order/components/Ord
|
||||
import { getOrderPriceAdjustConfig, adjustOrderPercent } from '#/api/order/priceAdjust';
|
||||
import type { QuickDiscountOption } from '#/utils/pricePercentAdjust';
|
||||
import { formatPriceDiscountLabel, normalizeQuickOptions } from '#/utils/pricePercentAdjust';
|
||||
|
||||
import { expressDetailByOrderId, getOrderInfo } from '../api';
|
||||
import { expressDetailByOrderId, getOrderInfo, syncLegacyShipmentApi } from '../api';
|
||||
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
|
||||
import { formatDoctorOrderText } from '#/utils/prescriptionDoctorOrder';
|
||||
import LogisticsModal from './logistics-modal.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'DetailModal',
|
||||
});
|
||||
|
||||
const userStore = useUserStore();
|
||||
const gridApi = ref();
|
||||
// 订单信息
|
||||
const data = ref();
|
||||
@@ -27,6 +29,20 @@ const data = ref();
|
||||
const expressDetail = ref({});
|
||||
// 订单发货方式
|
||||
const deliveryMethod = ref(-1);
|
||||
const syncingLegacy = ref(false);
|
||||
|
||||
/**
|
||||
* 超管可见:上门快递且订单有运单时,可手动把历史运单同步到 shipment
|
||||
* 与后端一致:仅 role_id=1(SUPPER_ADMIN)
|
||||
*/
|
||||
const canSyncLegacyShipment = computed(() => {
|
||||
const roleId = Number(
|
||||
userStore.userInfo?.role_id ?? userStore.userInfo?.roles?.id,
|
||||
);
|
||||
if (roleId !== 1) return false;
|
||||
if (Number(data.value?.delivery_method) !== 0) return false;
|
||||
return Number(data.value?.express_no_id) > 0;
|
||||
});
|
||||
|
||||
const [TraceDrawer, traceDrawerApi] = useVbenDrawer({
|
||||
connectedComponent: OrderTraceDrawer,
|
||||
@@ -36,6 +52,11 @@ const [PercentAdjustDrawer, percentAdjustDrawerApi] = useVbenDrawer({
|
||||
connectedComponent: OrderPricePercentAdjustDrawer,
|
||||
});
|
||||
|
||||
/** 详情内「查看物流动态」:独立弹窗只拉物流接口 */
|
||||
const [LogisticsModalComp, logisticsModalApi] = useVbenModal({
|
||||
connectedComponent: LogisticsModal,
|
||||
});
|
||||
|
||||
const priceAdjustMeta = ref({
|
||||
scope: 'sale_only' as 'both' | 'sale_only',
|
||||
quickOptions: [] as QuickDiscountOption[],
|
||||
@@ -114,7 +135,10 @@ const [Modal, modalApi] = useVbenModal({
|
||||
onOpenChange(isOpen: boolean) {
|
||||
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||
if (isOpen) {
|
||||
const { values, id } = modalApi.getData<Record<string, any>>();
|
||||
const modalData = modalApi.getData<Record<string, any>>() || {};
|
||||
const { values, id, onShipPackage: shipFn } = modalData;
|
||||
// 列表传入:按 warehouse_id 打开发货弹窗(平台包=0)
|
||||
onShipPackage.value = typeof shipFn === 'function' ? shipFn : null;
|
||||
if (id) {
|
||||
getOrderInfo(id).then((res) => {
|
||||
data.value = res;
|
||||
@@ -127,6 +151,8 @@ const [Modal, modalApi] = useVbenModal({
|
||||
deliveryMethod.value = data.value.delivery_method;
|
||||
getExpressDetail();
|
||||
}
|
||||
} else {
|
||||
onShipPackage.value = null;
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -138,26 +164,53 @@ async function getExpressDetail() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取物流信息标签颜色
|
||||
* @param status
|
||||
* 多包裹:优先物流接口 packages(门店角色已按开关抹仓名),
|
||||
* 避免订单详情未抹名数据盖过物流结果。
|
||||
*/
|
||||
function getColor(status: any) {
|
||||
switch (status) {
|
||||
case '在途': {
|
||||
return '';
|
||||
}
|
||||
case '揽收': {
|
||||
return 'orange';
|
||||
}
|
||||
case '派件': {
|
||||
return 'blue';
|
||||
}
|
||||
case '签收': {
|
||||
return 'green';
|
||||
}
|
||||
default: {
|
||||
return '';
|
||||
}
|
||||
const packageTabs = computed(() => {
|
||||
const fromExpress = Array.isArray((expressDetail.value as any)?.packages)
|
||||
? (expressDetail.value as any).packages
|
||||
: [];
|
||||
if (fromExpress.length) return fromExpress;
|
||||
const fromOrder = Array.isArray(data.value?.packages) ? data.value.packages : [];
|
||||
return fromOrder;
|
||||
});
|
||||
const activePkg = ref('0');
|
||||
/** 详情内「发本包」回调(由列表传入时仅平台包裹可发) */
|
||||
const onShipPackage = ref<null | ((warehouseId: number) => void)>(null);
|
||||
|
||||
/**
|
||||
* Tab 文案:有仓名才拼括号;空串不兜底「平台/仓」
|
||||
*/
|
||||
function packageTabLabel(pkg: Record<string, any>, idx: number): string {
|
||||
const no = pkg.package_no || idx + 1;
|
||||
const name = String(pkg.warehouse_name || '').trim();
|
||||
return name ? `包裹${no}(${name})` : `包裹${no}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开独立物流动态弹窗(完整轨迹)
|
||||
*/
|
||||
function openLogisticsModal() {
|
||||
if (!data.value?.id) return;
|
||||
logisticsModalApi.setData({ order_id: data.value.id });
|
||||
logisticsModalApi.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* 超管手动同步历史运单到分包裹表,成功后刷新详情与物流
|
||||
*/
|
||||
async function syncLegacyShipment() {
|
||||
if (!data.value?.id || syncingLegacy.value) return;
|
||||
syncingLegacy.value = true;
|
||||
try {
|
||||
const res = await syncLegacyShipmentApi(Number(data.value.id));
|
||||
message.success(res?.message || '同步成功');
|
||||
await reloadOrder();
|
||||
deliveryMethod.value = data.value?.delivery_method;
|
||||
await getExpressDetail();
|
||||
} finally {
|
||||
syncingLegacy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,45 +481,94 @@ function prescriptionStatusColor() {
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
<div v-if="deliveryMethod === 0">
|
||||
<div class="mt-4">
|
||||
<h3>物流信息</h3>
|
||||
<Descriptions
|
||||
:column="{ xxl: 2, xl: 2, lg: 2, md: 1, sm: 1, xs: 1 }"
|
||||
bordered
|
||||
class="mt-4"
|
||||
>
|
||||
<Descriptions.Item label="物流公司">
|
||||
{{ expressDetail.express_company_name }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="运单号">
|
||||
{{ expressDetail.express_no }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最新状态">
|
||||
{{ expressDetail.state_txt }}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<h3>物流追踪</h3>
|
||||
<Timeline class="mt-4">
|
||||
<Timeline.Item
|
||||
v-for="(detail, index) in expressDetail.detail"
|
||||
:key="index"
|
||||
<div v-if="deliveryMethod === 0" class="mt-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="mb-0">物流信息</h3>
|
||||
<Space>
|
||||
<Button
|
||||
v-if="canSyncLegacyShipment"
|
||||
type="link"
|
||||
size="small"
|
||||
:loading="syncingLegacy"
|
||||
@click="syncLegacyShipment"
|
||||
>
|
||||
<Tag :color="getColor(detail.status)">
|
||||
{{ detail.status }}
|
||||
</Tag>
|
||||
<p>{{ detail.detail_at }}</p>
|
||||
<p>{{ detail.detail }}</p>
|
||||
</Timeline.Item>
|
||||
</Timeline>
|
||||
同步物流包裹
|
||||
</Button>
|
||||
<Button type="link" size="small" @click="openLogisticsModal">
|
||||
查看物流动态
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<!-- 详情内仅保留包裹发货状态;完整轨迹进独立「物流动态」弹窗 -->
|
||||
<Tabs
|
||||
v-if="packageTabs.length"
|
||||
v-model:active-key="activePkg"
|
||||
type="card"
|
||||
class="mt-4"
|
||||
>
|
||||
<Tabs.TabPane
|
||||
v-for="(pkg, idx) in packageTabs"
|
||||
:key="String(idx)"
|
||||
:tab="packageTabLabel(pkg, idx)"
|
||||
>
|
||||
<div
|
||||
class="mb-3 flex justify-end"
|
||||
v-if="Number(pkg.is_send) === 0 && Number(pkg.warehouse_id) === 0 && onShipPackage"
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="onShipPackage(Number(pkg.warehouse_id) || 0)"
|
||||
>
|
||||
发本包裹
|
||||
</Button>
|
||||
</div>
|
||||
<template v-if="Number(pkg.is_send) === 1 && pkg.express">
|
||||
<Descriptions
|
||||
:column="{ xxl: 2, xl: 2, lg: 2, md: 1, sm: 1, xs: 1 }"
|
||||
bordered
|
||||
>
|
||||
<Descriptions.Item label="物流公司">
|
||||
{{ pkg.express.express_company_name || '-' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="运单号">
|
||||
{{ pkg.express.express_no || '-' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最新状态">
|
||||
{{ pkg.express.state_txt || '-' }}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</template>
|
||||
<div v-else class="text-gray-400 py-2">
|
||||
本包裹尚未发货
|
||||
<span v-if="Array.isArray(pkg.items) && pkg.items.length">
|
||||
({{ pkg.items.map((p: any) => p.drug_name || p.name).filter(Boolean).join('、') }})
|
||||
</span>
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
<Descriptions
|
||||
v-else
|
||||
:column="{ xxl: 2, xl: 2, lg: 2, md: 1, sm: 1, xs: 1 }"
|
||||
bordered
|
||||
class="mt-4"
|
||||
>
|
||||
<Descriptions.Item label="物流公司">
|
||||
{{ expressDetail.express_company_name || '-' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="运单号">
|
||||
{{ expressDetail.express_no || '-' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最新状态">
|
||||
{{ expressDetail.state_txt || '-' }}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<TraceDrawer />
|
||||
<PercentAdjustDrawer />
|
||||
<LogisticsModalComp />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -494,6 +596,14 @@ function prescriptionStatusColor() {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.mb-0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.space-x-4 > * + * {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 独立「物流动态」弹窗:只请求 express-detail,不展示整单其它信息。
|
||||
* 仓名为空时 Tab 仅显示「包裹N」,与后端诊所开关抹名约定一致。
|
||||
*/
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Descriptions, Spin, Tabs, Tag, Timeline } from 'ant-design-vue';
|
||||
|
||||
import { expressDetailByOrderId } from '../api';
|
||||
|
||||
defineOptions({
|
||||
name: 'ProductOrderLogisticsModal',
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const expressDetail = ref<Record<string, any>>({});
|
||||
const activePkg = ref('0');
|
||||
const orderId = ref<number | null>(null);
|
||||
|
||||
/**
|
||||
* 多包裹列表:以物流接口 packages 为准(门店角色已按开关抹仓名)
|
||||
*/
|
||||
const packageTabs = computed(() => {
|
||||
const pkgs = expressDetail.value?.packages;
|
||||
return Array.isArray(pkgs) ? pkgs : [];
|
||||
});
|
||||
|
||||
/**
|
||||
* Tab 文案:有仓名才拼括号,空串不兜底「平台/仓」
|
||||
*/
|
||||
function packageTabLabel(pkg: Record<string, any>, idx: number): string {
|
||||
const no = pkg.package_no || idx + 1;
|
||||
const name = String(pkg.warehouse_name || '').trim();
|
||||
return name ? `包裹${no}(${name})` : `包裹${no}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 物流状态 Tag 颜色
|
||||
*/
|
||||
function getColor(status: any) {
|
||||
switch (status) {
|
||||
case '在途': {
|
||||
return '';
|
||||
}
|
||||
case '揽收': {
|
||||
return 'orange';
|
||||
}
|
||||
case '派件': {
|
||||
return 'blue';
|
||||
}
|
||||
case '签收': {
|
||||
return 'green';
|
||||
}
|
||||
default: {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadExpress(id: number) {
|
||||
loading.value = true;
|
||||
try {
|
||||
expressDetail.value = (await expressDetailByOrderId({ order_id: id })) || {};
|
||||
activePkg.value = '0';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
showConfirmButton: false,
|
||||
cancelText: '关闭',
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
if (isOpen) {
|
||||
const payload = modalApi.getData<Record<string, any>>() || {};
|
||||
const id = Number(payload.order_id || payload.id || 0);
|
||||
orderId.value = id || null;
|
||||
expressDetail.value = {};
|
||||
if (id) {
|
||||
loadExpress(id);
|
||||
}
|
||||
} else {
|
||||
orderId.value = null;
|
||||
expressDetail.value = {};
|
||||
activePkg.value = '0';
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-[640px]" title="物流动态">
|
||||
<Spin :spinning="loading">
|
||||
<div v-if="!orderId" class="text-gray-400 py-6 text-center">
|
||||
缺少订单信息
|
||||
</div>
|
||||
<template v-else>
|
||||
<Tabs
|
||||
v-if="packageTabs.length"
|
||||
v-model:active-key="activePkg"
|
||||
type="card"
|
||||
>
|
||||
<Tabs.TabPane
|
||||
v-for="(pkg, idx) in packageTabs"
|
||||
:key="String(idx)"
|
||||
:tab="packageTabLabel(pkg, idx)"
|
||||
>
|
||||
<template v-if="Number(pkg.is_send) === 1 && pkg.express">
|
||||
<Descriptions
|
||||
:column="{ xxl: 2, xl: 2, lg: 2, md: 1, sm: 1, xs: 1 }"
|
||||
bordered
|
||||
>
|
||||
<Descriptions.Item label="物流公司">
|
||||
{{ pkg.express.express_company_name || '-' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="运单号">
|
||||
{{ pkg.express.express_no || '-' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最新状态">
|
||||
{{ pkg.express.state_txt || '-' }}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<div class="mt-4">
|
||||
<h4 class="mb-2">物流追踪</h4>
|
||||
<Timeline
|
||||
v-if="Array.isArray(pkg.express.detail) && pkg.express.detail.length"
|
||||
>
|
||||
<Timeline.Item
|
||||
v-for="(detail, dIdx) in pkg.express.detail"
|
||||
:key="dIdx"
|
||||
>
|
||||
<Tag :color="getColor(detail.status)">{{ detail.status }}</Tag>
|
||||
<p>{{ detail.detail_at }}</p>
|
||||
<p>{{ detail.detail }}</p>
|
||||
</Timeline.Item>
|
||||
</Timeline>
|
||||
<div v-else class="text-gray-400">暂无物流轨迹</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="text-gray-400 py-2">本包裹尚未发货</div>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
<template v-else>
|
||||
<Descriptions
|
||||
:column="{ xxl: 2, xl: 2, lg: 2, md: 1, sm: 1, xs: 1 }"
|
||||
bordered
|
||||
>
|
||||
<Descriptions.Item label="物流公司">
|
||||
{{ expressDetail.express_company_name || '-' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="运单号">
|
||||
{{ expressDetail.express_no || '-' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最新状态">
|
||||
{{ expressDetail.state_txt || '-' }}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<div class="mt-4">
|
||||
<h4 class="mb-2">物流追踪</h4>
|
||||
<Timeline
|
||||
v-if="Array.isArray(expressDetail.detail) && expressDetail.detail.length"
|
||||
>
|
||||
<Timeline.Item
|
||||
v-for="(detail, index) in expressDetail.detail"
|
||||
:key="index"
|
||||
>
|
||||
<Tag :color="getColor(detail.status)">{{ detail.status }}</Tag>
|
||||
<p>{{ detail.detail_at }}</p>
|
||||
<p>{{ detail.detail }}</p>
|
||||
</Timeline.Item>
|
||||
</Timeline>
|
||||
<div v-else class="text-gray-400">暂无物流轨迹</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</Spin>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mt-4 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -27,9 +27,16 @@ const [Modal, modalApi] = useVbenModal({
|
||||
formApi.validate().then(async (e: any) => {
|
||||
if (e.valid) {
|
||||
const values = await formApi.getValues();
|
||||
// 仅提交合法非负整数 warehouse_id;脏值不传,由后端按角色默认平台包 0
|
||||
const wid = Number(values.warehouse_id);
|
||||
const payload: Record<string, any> = { ...values };
|
||||
if (Number.isFinite(wid) && wid >= 0 && String(values.warehouse_id) !== '[object Object]') {
|
||||
payload.warehouse_id = wid;
|
||||
} else {
|
||||
delete payload.warehouse_id;
|
||||
}
|
||||
modalApi.setState({ loading: true, confirmLoading: true });
|
||||
const submitApi = sendOrder;
|
||||
submitApi(values)
|
||||
sendOrder(payload)
|
||||
.then(() => {
|
||||
message.success('发货成功');
|
||||
gridApi.value?.reload();
|
||||
@@ -45,7 +52,9 @@ const [Modal, modalApi] = useVbenModal({
|
||||
onOpenChange(isOpen: boolean) {
|
||||
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||
if (isOpen) {
|
||||
const { values, update } = modalApi.getData<Record<string, any>>();
|
||||
const { values, update } = modalApi.getData<Record<string, any>>() || {};
|
||||
// 先重置,避免上次发货残留的 warehouse_id
|
||||
formApi.resetForm();
|
||||
if (values) {
|
||||
orderNo.value = values.order_no;
|
||||
isUpdate.value = update;
|
||||
|
||||
@@ -22,6 +22,17 @@ export const modalFormProps: VbenFormProps = {
|
||||
triggerFields: ['oreder_id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
// 分包裹发货:0=平台包;>0=配送仓(默认 0)
|
||||
component: 'VbenInput',
|
||||
fieldName: 'warehouse_id',
|
||||
label: '发货方',
|
||||
defaultValue: 0,
|
||||
dependencies: {
|
||||
show: false,
|
||||
triggerFields: ['warehouse_id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { VbenFormProps } from '#/adapter/form';
|
||||
import type { VxeGridListeners } from '#/adapter/vxe-table';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import {
|
||||
@@ -15,7 +15,18 @@ import {
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import { SvgCakeIcon } from '@vben/icons';
|
||||
|
||||
import { Button, Image, message, Modal as AntdModal, Popconfirm, Popover, Space, Switch, Table, Tag } from 'ant-design-vue';
|
||||
import {
|
||||
Button,
|
||||
Image,
|
||||
message,
|
||||
Modal as AntdModal,
|
||||
Popconfirm,
|
||||
Popover,
|
||||
Space,
|
||||
Switch,
|
||||
Table,
|
||||
Tag,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
@@ -24,12 +35,14 @@ import { TableAction } from '#/components/table-action';
|
||||
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
|
||||
import {
|
||||
cancelOrderApi,
|
||||
changeDeliveryWarehouseApi,
|
||||
getOrderInfo,
|
||||
getOrderList,
|
||||
getVerifyRecentOrderAmounts,
|
||||
saleAmountApi,
|
||||
updateFreeShipping,
|
||||
} from '#/views/business/order/product-order/api';
|
||||
import { getDeliveryWarehouseOptionsByDrugs } from '#/views/doctor/doctor-reception/api';
|
||||
import { simulatePayApi, accrueSalespersonCommissionApi, reverseSalespersonCommissionApi } from '#/views/business/order/api/order-ops';
|
||||
import ChinaErpSyncLogDrawer from '#/views/business/order/components/china-erp-sync-log-drawer.vue';
|
||||
import OrderTraceDrawer from '#/views/business/order/components/order-trace-drawer.vue';
|
||||
@@ -42,6 +55,7 @@ import DoctorCardModal from '#/views/doctor/doctor/components/DoctorCardModal.vu
|
||||
|
||||
import DetailModal from './components/detail.vue';
|
||||
import OrderUserInfoCell from './components/cells/OrderUserInfoCell.vue';
|
||||
import LogisticsModal from './components/logistics-modal.vue';
|
||||
import FormModalDemo from './components/modal.vue';
|
||||
import ProductOrderExportModal from './components/ProductOrderExportModal.vue';
|
||||
import Refund from './components/refund.vue';
|
||||
@@ -167,6 +181,10 @@ const [FormModal, formModalApi] = useVbenModal({
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
connectedComponent: DetailModal,
|
||||
});
|
||||
/** 独立物流动态弹窗:只拉 express-detail,与订单详情解耦 */
|
||||
const [LogisticsModalComp, logisticsModalApi] = useVbenModal({
|
||||
connectedComponent: LogisticsModal,
|
||||
});
|
||||
const [RefundModal, RefundModalApi] = useVbenModal({
|
||||
connectedComponent: Refund,
|
||||
});
|
||||
@@ -175,22 +193,51 @@ const [ExportModal, exportModalApi] = useVbenModal({
|
||||
connectedComponent: ProductOrderExportModal,
|
||||
});
|
||||
|
||||
const infoModal = (data = {}) => {
|
||||
const infoModal = (data: Record<string, any> = {}) => {
|
||||
modalApi.setData({
|
||||
// 表单值
|
||||
// 带 id 走详情接口拿到 packages;values 作首屏兜底
|
||||
id: data?.id,
|
||||
values: data,
|
||||
gridApi,
|
||||
// 详情内「发本包裹」:关详情后打开发货弹窗(仅平台包会传 0)
|
||||
onShipPackage: (warehouseId: number) => {
|
||||
modalApi.close();
|
||||
wareSend(data, warehouseId);
|
||||
},
|
||||
});
|
||||
modalApi.open();
|
||||
};
|
||||
|
||||
const wareSend = (data = {}) => {
|
||||
/**
|
||||
* 打开物流动态:仅传 order_id,弹窗内单独请求物流接口
|
||||
*/
|
||||
const openLogisticsModal = (data: Record<string, any> = {}) => {
|
||||
logisticsModalApi.setData({
|
||||
order_id: data?.id,
|
||||
});
|
||||
logisticsModalApi.open();
|
||||
};
|
||||
|
||||
/**
|
||||
* 打开发货弹窗。
|
||||
* 列表不传 warehouse_id(后端平台默认 0);详情按包发时仅传合法非负整数。
|
||||
* 注意:勿用 bind(null,row),table-action 的 click 会把事件当成第 2 参污染 warehouse_id。
|
||||
*/
|
||||
const wareSend = (data: Record<string, any> = {}, warehouseId?: number) => {
|
||||
const values: Record<string, any> = {
|
||||
order_id: data?.id,
|
||||
order_no: data?.order_no,
|
||||
};
|
||||
if (
|
||||
warehouseId !== undefined &&
|
||||
warehouseId !== null &&
|
||||
Number.isFinite(Number(warehouseId)) &&
|
||||
Number(warehouseId) >= 0
|
||||
) {
|
||||
values.warehouse_id = Number(warehouseId);
|
||||
}
|
||||
formModalApi.setData({
|
||||
// 表单值
|
||||
values: {
|
||||
order_id: data?.id,
|
||||
order_no: data?.order_no,
|
||||
},
|
||||
values,
|
||||
gridApi,
|
||||
});
|
||||
formModalApi.open();
|
||||
@@ -450,6 +497,152 @@ async function handleCancelOrder(row: Record<string, any>) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 订单页改仓弹窗状态 */
|
||||
const changeWhOpen = ref(false);
|
||||
const changeWhSubmitting = ref(false);
|
||||
const changeWhLoading = ref(false);
|
||||
const changeWhOrderId = ref(0);
|
||||
const changeWhFromId = ref(0);
|
||||
const changeWhFromName = ref('');
|
||||
const changeWhToId = ref<number | undefined>(undefined);
|
||||
const changeWhOptions = ref<
|
||||
Array<{ warehouse_id: number; warehouse_name: string; quote: string }>
|
||||
>([]);
|
||||
|
||||
/**
|
||||
* 改仓卡片选项:首项本仓库 + 可选配送仓
|
||||
* 当前已是本仓时本仓库卡片 disabled
|
||||
*/
|
||||
const changeWhCardOptions = computed(() => {
|
||||
const local = {
|
||||
warehouse_id: 0,
|
||||
warehouse_name: '萧康医药本仓库',
|
||||
quote: null as string | null,
|
||||
disabled: changeWhFromId.value === 0,
|
||||
};
|
||||
return [
|
||||
local,
|
||||
...changeWhOptions.value.map((o) => ({
|
||||
...o,
|
||||
quote: o.quote as string | null,
|
||||
disabled: false,
|
||||
})),
|
||||
];
|
||||
});
|
||||
|
||||
/**
|
||||
* 将 options-by-drugs 按仓去重(取最低 quote),供改仓下拉使用
|
||||
*/
|
||||
function flattenWarehouseOptions(
|
||||
map: Record<string, any[]> | null | undefined,
|
||||
): Array<{ warehouse_id: number; warehouse_name: string; quote: string }> {
|
||||
const byId = new Map<
|
||||
number,
|
||||
{ warehouse_id: number; warehouse_name: string; quote: string }
|
||||
>();
|
||||
if (!map || typeof map !== 'object') {
|
||||
return [];
|
||||
}
|
||||
for (const opts of Object.values(map)) {
|
||||
if (!Array.isArray(opts)) {
|
||||
continue;
|
||||
}
|
||||
for (const opt of opts) {
|
||||
const id = Number(opt?.warehouse_id ?? 0);
|
||||
if (id <= 0) {
|
||||
continue;
|
||||
}
|
||||
const quote = String(opt?.quote ?? '0');
|
||||
const prev = byId.get(id);
|
||||
if (!prev || Number(quote) < Number(prev.quote)) {
|
||||
byId.set(id, {
|
||||
warehouse_id: id,
|
||||
warehouse_name: String(opt?.warehouse_name ?? `仓库#${id}`),
|
||||
quote,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...byId.values()].sort((a, b) => Number(a.quote) - Number(b.quote));
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击仓库 Tag:打开改仓弹窗(首项本仓库 + 配送仓列表)
|
||||
*/
|
||||
async function openChangeWarehouse(
|
||||
row: Record<string, any>,
|
||||
wh: { id: number; name: string },
|
||||
) {
|
||||
changeWhOrderId.value = Number(row.id ?? 0);
|
||||
changeWhFromId.value = Number(wh?.id ?? 0);
|
||||
changeWhFromName.value = String(wh?.name ?? '');
|
||||
changeWhToId.value = undefined;
|
||||
changeWhOptions.value = [];
|
||||
changeWhOpen.value = true;
|
||||
changeWhLoading.value = true;
|
||||
try {
|
||||
const items = Array.isArray(row.product_order_items)
|
||||
? row.product_order_items
|
||||
: [];
|
||||
// 优先取当前仓名下药品;若无 delivery_warehouse_id 字段则退回整单药品
|
||||
const fromId = changeWhFromId.value;
|
||||
const scoped = items.filter((it: any) => {
|
||||
if (it?.delivery_warehouse_id === undefined || it?.delivery_warehouse_id === null) {
|
||||
return true;
|
||||
}
|
||||
const wid = Number(it.delivery_warehouse_id ?? 0);
|
||||
return fromId > 0 ? wid === fromId : wid <= 0;
|
||||
});
|
||||
const drugIds = [
|
||||
...new Set(
|
||||
(scoped.length ? scoped : items)
|
||||
.map((it: any) => Number(it?.drug_id ?? 0))
|
||||
.filter((id: number) => id > 0),
|
||||
),
|
||||
];
|
||||
if (drugIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
const res = await getDeliveryWarehouseOptionsByDrugs({ drug_ids: drugIds });
|
||||
const flattened = flattenWarehouseOptions(res);
|
||||
// 排除当前仓,避免无意义提交
|
||||
changeWhOptions.value = flattened.filter(
|
||||
(o) => o.warehouse_id !== changeWhFromId.value,
|
||||
);
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || '加载配送仓库失败');
|
||||
} finally {
|
||||
changeWhLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 确认改仓 */
|
||||
async function submitChangeWarehouse() {
|
||||
if (changeWhToId.value === undefined || changeWhToId.value === null) {
|
||||
message.warning('请选择目标仓库');
|
||||
return;
|
||||
}
|
||||
if (Number(changeWhToId.value) === Number(changeWhFromId.value)) {
|
||||
message.warning('目标仓库与当前仓库相同');
|
||||
return;
|
||||
}
|
||||
changeWhSubmitting.value = true;
|
||||
try {
|
||||
await changeDeliveryWarehouseApi({
|
||||
order_id: changeWhOrderId.value,
|
||||
from_warehouse_id: changeWhFromId.value,
|
||||
to_warehouse_id: Number(changeWhToId.value),
|
||||
});
|
||||
message.success('改仓成功');
|
||||
changeWhOpen.value = false;
|
||||
await gridApi.query();
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || '改仓失败');
|
||||
} finally {
|
||||
changeWhSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const toggleFreeShipping = async (row: any, checked: boolean) => {
|
||||
try {
|
||||
const res = await updateFreeShipping({ id: row.id, is_free_shipping: checked ? 1 : 0 });
|
||||
@@ -511,6 +704,7 @@ const openOrderAmountVerify = () => {
|
||||
<Page auto-content-height title="订单管理">
|
||||
<FormModal />
|
||||
<Modal />
|
||||
<LogisticsModalComp />
|
||||
<AntdModal
|
||||
v-model:open="verifyOpen"
|
||||
title="近10分钟订单金额校验"
|
||||
@@ -643,7 +837,7 @@ const openOrderAmountVerify = () => {
|
||||
<div class="text-xs text-gray-500">{{ formatExpressAddress(row) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 患者配送仓库(一单多仓用 Tag 展示) -->
|
||||
<!-- 患者配送仓库:点击 Tag 可改仓(本仓库 / 配送仓) -->
|
||||
<template #delivery-warehouses="{ row }">
|
||||
<div
|
||||
v-if="Array.isArray(row.delivery_warehouses) && row.delivery_warehouses.length"
|
||||
@@ -653,6 +847,8 @@ const openOrderAmountVerify = () => {
|
||||
v-for="wh in row.delivery_warehouses"
|
||||
:key="wh.id"
|
||||
color="processing"
|
||||
class="cursor-pointer"
|
||||
@click="openChangeWarehouse(row, wh)"
|
||||
>
|
||||
{{ wh.name }}
|
||||
</Tag>
|
||||
@@ -974,6 +1170,19 @@ const openOrderAmountVerify = () => {
|
||||
// auth: ['order', 'sys:role:detail'],
|
||||
onClick: infoModal.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '物流动态',
|
||||
type: 'link',
|
||||
icon: 'mdi:truck-delivery-outline',
|
||||
size: 'small',
|
||||
// 上门快递且已支付、非取消/待支付:可单独查看物流
|
||||
ifShow:
|
||||
row.delivery_method === 0 &&
|
||||
row.is_pay === 1 &&
|
||||
row.status !== 0 &&
|
||||
row.status !== 9,
|
||||
onClick: openLogisticsModal.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '溯源',
|
||||
type: 'link',
|
||||
@@ -1002,11 +1211,7 @@ const openOrderAmountVerify = () => {
|
||||
type: 'link',
|
||||
icon: 'ri:send-plane-fill',
|
||||
auth: ['Super Admin','Admin'],
|
||||
onClick: wareSend.bind(null, row),
|
||||
// popConfirm: {
|
||||
// title: '确定发货吗?',
|
||||
// confirm: wareSend.bind(null, row),
|
||||
// },
|
||||
onClick: () => wareSend(row),
|
||||
},
|
||||
]"
|
||||
:drop-down-actions="[
|
||||
@@ -1144,6 +1349,54 @@ const openOrderAmountVerify = () => {
|
||||
</template>
|
||||
</Grid>
|
||||
<PercentAdjustDrawer />
|
||||
<!-- 订单改仓:卡片选择(本仓库 + 配送仓) -->
|
||||
<AntdModal
|
||||
v-model:open="changeWhOpen"
|
||||
title="修改配送仓库"
|
||||
:confirm-loading="changeWhSubmitting"
|
||||
destroy-on-close
|
||||
@ok="submitChangeWarehouse"
|
||||
>
|
||||
<div class="text-muted-foreground mb-3 text-sm">
|
||||
当前仓库:{{ changeWhFromName || '—' }}
|
||||
</div>
|
||||
<div v-if="changeWhLoading" class="text-muted-foreground py-4 text-center">
|
||||
加载可选仓库…
|
||||
</div>
|
||||
<div v-else class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="opt in changeWhCardOptions"
|
||||
:key="opt.warehouse_id"
|
||||
type="button"
|
||||
class="min-w-[140px] rounded-lg border px-3 py-2 text-left transition-colors"
|
||||
:class="[
|
||||
opt.disabled
|
||||
? 'border-border cursor-not-allowed opacity-50'
|
||||
: changeWhToId === opt.warehouse_id
|
||||
? 'border-primary bg-primary/10 ring-primary ring-1'
|
||||
: 'border-border hover:border-primary/50',
|
||||
]"
|
||||
:disabled="opt.disabled"
|
||||
@click="changeWhToId = opt.warehouse_id"
|
||||
>
|
||||
<div class="text-foreground text-sm font-medium">
|
||||
{{ opt.warehouse_name }}
|
||||
</div>
|
||||
<div
|
||||
v-if="opt.quote != null"
|
||||
class="text-muted-foreground mt-1 text-xs"
|
||||
>
|
||||
供货价 {{ opt.quote }}
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
v-if="changeWhOptions.length === 0"
|
||||
class="text-muted-foreground w-full text-sm"
|
||||
>
|
||||
暂无其它可用配送仓库
|
||||
</div>
|
||||
</div>
|
||||
</AntdModal>
|
||||
</Page>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
@@ -7,9 +7,18 @@ import { Button } from 'ant-design-vue';
|
||||
|
||||
import { getPrescriptionInfoApi } from '#/views/doctor/doctor-reception/api';
|
||||
import { formatDoctorOrderText } from '#/utils/prescriptionDoctorOrder';
|
||||
import { formatStoreNameWithHu } from '#/utils/formatStoreNameWithHu';
|
||||
|
||||
const item = ref<Record<string, any>>({});
|
||||
|
||||
/** 顶栏诊所名:在线渠道追加(互);无诊所名时兜底空串 */
|
||||
const clinicTitleName = computed(() =>
|
||||
formatStoreNameWithHu(
|
||||
item.value?.store?.name || item.value?.content?.store?.name || '',
|
||||
item.value?.is_online,
|
||||
),
|
||||
);
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
@@ -128,7 +137,7 @@ function parseRecipeContent(content: string | Record<string, unknown>) {
|
||||
<span>处方编号: {{ item.prescription_no }}</span>
|
||||
<div class="prescription-type">普通处方</div>
|
||||
</div>
|
||||
<h2 class="clinic-name">{{ item.content?.patient.name }} 处方笺</h2>
|
||||
<h2 class="clinic-name">{{ clinicTitleName || '诊所' }} 处方笺</h2>
|
||||
<div class="prescription-date">开具日期: {{ item.created_at }}</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -71,16 +71,22 @@ const setVisible = (value: boolean, instruction = '') => {
|
||||
previewImage.value = instruction;
|
||||
};
|
||||
|
||||
// 本地存储键名(支持前缀区分不同场景)
|
||||
// 本地存储键名:与 prescription store 一致(prefix 已含尾部 `-`,勿再拼 `-`)
|
||||
const storageKey = computed(() => {
|
||||
const prefix = storagePrefix.value ? `${storagePrefix.value}-` : '';
|
||||
return `${prefix}prescriptionData_${type.value}_${activePatientId.value}`;
|
||||
return `${storagePrefix.value}prescriptionData_${type.value}_${activePatientId.value}`;
|
||||
});
|
||||
|
||||
// 获取当前患者的药品数据
|
||||
/**
|
||||
* 同步已选药品到 selectList
|
||||
* chat 模式以 Pinia currentDrugs 为准,避免双横线孤儿缓存回填
|
||||
*/
|
||||
const getCurrentDrugs = () => {
|
||||
try {
|
||||
// 从localStorage获取数据并解析
|
||||
if (isChatMode.value) {
|
||||
const drugs = prescriptionStore.currentDrugs;
|
||||
selectList.value = Array.isArray(drugs) ? [...drugs] : [];
|
||||
return;
|
||||
}
|
||||
const storedData = localStorage.getItem(storageKey.value);
|
||||
selectList.value = storedData ? JSON.parse(storedData) : [];
|
||||
} catch (error) {
|
||||
@@ -167,6 +173,8 @@ function addProducts(data: any) {
|
||||
number: 1,
|
||||
price: data.price,
|
||||
image: data.drug.image,
|
||||
// 规格(选仓弹窗等处展示)
|
||||
specification: data.drug.specification || data.specification || '',
|
||||
instruction: data.drug.instruction,
|
||||
type: data.drug.type,
|
||||
select_number: 1,
|
||||
@@ -316,7 +324,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
// 提示成功
|
||||
message.success('保存成功');
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
onOpenChange: async (isOpen: boolean) => {
|
||||
if (isOpen) {
|
||||
// 获取回调函数
|
||||
const data = modalApi.getData();
|
||||
@@ -334,8 +342,15 @@ const [Modal, modalApi] = useVbenModal({
|
||||
activePatientId.value = activePatient_id;
|
||||
if (isChat === 'chat') {
|
||||
isChatMode.value = true;
|
||||
// 使用轻量级初始化,不获取患者信息
|
||||
initializeForModal(activePatient_id);
|
||||
await initializeForModal(
|
||||
activePatient_id,
|
||||
prefix || 'onlineConsultation-',
|
||||
);
|
||||
// 弹窗类型与 store tab 对齐,避免读到其它分类的已选
|
||||
prescriptionStore.activeCategory = type.value;
|
||||
prescriptionStore.loadFromLocalStorage();
|
||||
const drugs = prescriptionStore.currentDrugs;
|
||||
selectList.value = Array.isArray(drugs) ? [...drugs] : [];
|
||||
} else {
|
||||
isChatMode.value = false;
|
||||
}
|
||||
|
||||
@@ -77,16 +77,22 @@ const setVisible = (value, instruction = '') => {
|
||||
previewImage.value = instruction;
|
||||
};
|
||||
|
||||
// 本地存储键名(支持前缀区分不同场景)
|
||||
// 本地存储键名:与 prescription store 一致(prefix 已含尾部 `-`,勿再拼 `-`)
|
||||
const storageKey = computed(() => {
|
||||
const prefix = storagePrefix.value ? `${storagePrefix.value}-` : '';
|
||||
return `${prefix}prescriptionData_${type.value}_${activePatientId.value}`;
|
||||
return `${storagePrefix.value}prescriptionData_${type.value}_${activePatientId.value}`;
|
||||
});
|
||||
|
||||
// 获取当前患者的药品数据
|
||||
/**
|
||||
* 同步已选药品到 selectList
|
||||
* chat 模式以 Pinia currentDrugs 为准(发送后已清空),避免孤儿 localStorage 回填
|
||||
*/
|
||||
const getCurrentDrugs = () => {
|
||||
try {
|
||||
// 从localStorage获取数据并解析
|
||||
if (ChatTypeCheck.value === 'chat') {
|
||||
const drugs = prescriptionStore.currentDrugs;
|
||||
selectList.value = Array.isArray(drugs) ? [...drugs] : [];
|
||||
return;
|
||||
}
|
||||
const storedData = localStorage.getItem(storageKey.value);
|
||||
selectList.value = storedData ? JSON.parse(storedData) : [];
|
||||
} catch (error) {
|
||||
@@ -202,6 +208,8 @@ function addProducts(data) {
|
||||
unit_id: data.drug.unit_id,
|
||||
// 药品图片
|
||||
image: data.drug.image,
|
||||
// 规格(选仓弹窗等处展示)
|
||||
specification: data.drug.specification || data.specification || '',
|
||||
// 药品说明书
|
||||
instruction: data.drug.instruction,
|
||||
// 药品类型
|
||||
@@ -376,7 +384,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
// 提示成功
|
||||
message.success('保存成功');
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
onOpenChange: async (isOpen: boolean) => {
|
||||
if (isOpen) {
|
||||
// 获取回调函数
|
||||
const data = modalApi.getData();
|
||||
@@ -394,8 +402,18 @@ const [Modal, modalApi] = useVbenModal({
|
||||
activePatientId.value = activePatient_id;
|
||||
if (isChat === 'chat') {
|
||||
ChatTypeCheck.value = isChat;
|
||||
// 使用轻量级初始化,不获取患者信息
|
||||
initializeForModal(activePatient_id);
|
||||
// 传入 prefix,与 store 读写同一套 key;完成后用 store 回填已选
|
||||
await initializeForModal(
|
||||
activePatient_id,
|
||||
prefix || 'onlineConsultation-',
|
||||
);
|
||||
// 弹窗类型可能与 initialize 恢复的 tab 不一致,强制对齐后再取已选
|
||||
prescriptionStore.activeCategory = type.value;
|
||||
prescriptionStore.loadFromLocalStorage();
|
||||
const drugs = prescriptionStore.currentDrugs;
|
||||
selectList.value = Array.isArray(drugs) ? [...drugs] : [];
|
||||
} else {
|
||||
ChatTypeCheck.value = '';
|
||||
}
|
||||
// 获取药品列表
|
||||
getDrugListByWesternModal();
|
||||
|
||||
@@ -3,7 +3,7 @@ import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button, Descriptions, Image, Tag, Timeline } from 'ant-design-vue';
|
||||
import { Button, Descriptions, Image, Tabs, Tag, Timeline } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
expressDetailByOrderId,
|
||||
@@ -21,9 +21,23 @@ const logisticsAnchorRef = ref<HTMLElement | null>(null);
|
||||
/** 待发货时由列表传入,点击后关详情并打开发货弹窗 */
|
||||
const onShipFn = ref<null | (() => void)>(null);
|
||||
|
||||
const canShip = computed(
|
||||
() => Number(data.value.status) === 1 && typeof onShipFn.value === 'function',
|
||||
);
|
||||
const canShip = computed(() => {
|
||||
const packages = Array.isArray(data.value.packages) ? data.value.packages : [];
|
||||
const mineUnsent = packages.some((p: any) => Number(p.is_send) === 0);
|
||||
const legacy =
|
||||
Number(data.value.warehouse_is_send ?? 0) === 0 &&
|
||||
Number(data.value.status) === 1;
|
||||
return (mineUnsent || legacy) && typeof onShipFn.value === 'function';
|
||||
});
|
||||
const packageTabs = computed(() => {
|
||||
const packages = Array.isArray(data.value.packages)
|
||||
? data.value.packages
|
||||
: Array.isArray(expressDetail.value.packages)
|
||||
? expressDetail.value.packages
|
||||
: [];
|
||||
return packages;
|
||||
});
|
||||
const activePkg = ref('0');
|
||||
|
||||
const statusMap: Record<number, { text: string; color: string }> = {
|
||||
0: { text: '待支付', color: 'default' },
|
||||
@@ -219,8 +233,50 @@ const [Modal, modalApi] = useVbenModal({
|
||||
|
||||
<section ref="logisticsAnchorRef">
|
||||
<h3 class="section-title">物流信息</h3>
|
||||
<Tabs v-if="packageTabs.length" v-model:active-key="activePkg" type="card">
|
||||
<Tabs.TabPane
|
||||
v-for="(pkg, idx) in packageTabs"
|
||||
:key="String(idx)"
|
||||
:tab="`包裹${pkg.package_no || idx + 1}(${pkg.warehouse_name || '本仓'})`"
|
||||
>
|
||||
<template v-if="Number(pkg.is_send) === 1 && pkg.express">
|
||||
<Descriptions
|
||||
:column="{ xxl: 2, xl: 2, lg: 2, md: 1, sm: 1, xs: 1 }"
|
||||
bordered
|
||||
size="small"
|
||||
>
|
||||
<Descriptions.Item label="物流公司">
|
||||
{{ pkg.express.express_company_name || '-' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="运单号">
|
||||
{{ pkg.express.express_no || '-' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最新状态">
|
||||
{{ pkg.express.state_txt || '-' }}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<div
|
||||
v-if="Array.isArray(pkg.express.detail) && pkg.express.detail.length"
|
||||
class="mt-4"
|
||||
>
|
||||
<Timeline>
|
||||
<Timeline.Item
|
||||
v-for="(detail, dIdx) in pkg.express.detail"
|
||||
:key="dIdx"
|
||||
>
|
||||
<Tag :color="getColor(detail.status)">{{ detail.status }}</Tag>
|
||||
<p>{{ detail.detail_at }}</p>
|
||||
<p>{{ detail.detail }}</p>
|
||||
</Timeline.Item>
|
||||
</Timeline>
|
||||
</div>
|
||||
<div v-else class="empty-tip mt-3">暂无物流轨迹</div>
|
||||
</template>
|
||||
<div v-else class="empty-tip">本包裹尚未发货</div>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
<template
|
||||
v-if="
|
||||
v-else-if="
|
||||
Number(data.is_send) === 1 ||
|
||||
Number(data.status) >= 2 ||
|
||||
expressNoRow().express_no ||
|
||||
@@ -249,21 +305,17 @@ const [Modal, modalApi] = useVbenModal({
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<div v-if="Array.isArray(expressDetail.detail) && expressDetail.detail.length" class="mt-4">
|
||||
<h4 class="mb-2 text-sm font-medium">物流追踪</h4>
|
||||
<Timeline>
|
||||
<Timeline.Item
|
||||
v-for="(detail, index) in expressDetail.detail"
|
||||
:key="index"
|
||||
>
|
||||
<Tag :color="getColor(detail.status)">
|
||||
{{ detail.status }}
|
||||
</Tag>
|
||||
<Tag :color="getColor(detail.status)">{{ detail.status }}</Tag>
|
||||
<p>{{ detail.detail_at }}</p>
|
||||
<p>{{ detail.detail }}</p>
|
||||
</Timeline.Item>
|
||||
</Timeline>
|
||||
</div>
|
||||
<div v-else class="empty-tip mt-3">暂无物流轨迹</div>
|
||||
</template>
|
||||
<div v-else class="empty-tip">暂无物流</div>
|
||||
</section>
|
||||
|
||||
@@ -120,7 +120,7 @@ function openDetail(row: Record<string, any>, scrollToLogistics = false) {
|
||||
id: row?.id,
|
||||
scrollToLogistics,
|
||||
onShip:
|
||||
Number(row?.status) === 1
|
||||
Number(row?.warehouse_is_send ?? 0) === 0 || Number(row?.status) === 1
|
||||
? () => {
|
||||
detailModalApi.close();
|
||||
wareSend(row);
|
||||
@@ -284,7 +284,7 @@ onMounted(() => {
|
||||
物流
|
||||
</Button>
|
||||
<Button
|
||||
v-if="Number(row.status) === 1"
|
||||
v-if="Number(row.warehouse_is_send ?? 0) === 0"
|
||||
size="small"
|
||||
type="link"
|
||||
@click="wareSend(row)"
|
||||
|
||||
@@ -31,14 +31,28 @@ const newQuickName = ref('');
|
||||
const newQuickValue = ref<number | null>(null);
|
||||
const inputAutoAuditPass = ref(true);
|
||||
const miniprogramEmptyImage = ref('');
|
||||
/** 患者端物流是否显示配送仓名(默认否) */
|
||||
const logisticsShowWhNameUser = ref(false);
|
||||
/** 诊所端物流是否显示配送仓名(默认是) */
|
||||
const logisticsShowWhNameClinic = ref(true);
|
||||
|
||||
function readStoredTab() {
|
||||
const stored = localStorage.getItem(TAB_STORAGE_KEY);
|
||||
if (stored === 'price_adjust' || stored === 'input_audit' || stored === 'miniprogram') {
|
||||
if (
|
||||
stored === 'price_adjust' ||
|
||||
stored === 'input_audit' ||
|
||||
stored === 'miniprogram' ||
|
||||
stored === 'logistics'
|
||||
) {
|
||||
activeKey.value = stored;
|
||||
}
|
||||
}
|
||||
|
||||
function parseBoolConfig(val: unknown, defaultVal: boolean): boolean {
|
||||
if (val === undefined || val === null || val === '') return defaultVal;
|
||||
return val === '1' || val === 1 || val === true || val === 'true';
|
||||
}
|
||||
|
||||
function handleTabChange(key: string | number) {
|
||||
activeKey.value = String(key);
|
||||
localStorage.setItem(TAB_STORAGE_KEY, String(key));
|
||||
@@ -84,6 +98,12 @@ async function load() {
|
||||
if (row.config_key === 'miniprogram_empty_image') {
|
||||
miniprogramEmptyImage.value = String(row.config_value || '');
|
||||
}
|
||||
if (row.config_key === 'logistics_show_warehouse_name_user') {
|
||||
logisticsShowWhNameUser.value = parseBoolConfig(row.config_value, false);
|
||||
}
|
||||
if (row.config_key === 'logistics_show_warehouse_name_clinic') {
|
||||
logisticsShowWhNameClinic.value = parseBoolConfig(row.config_value, true);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
@@ -125,6 +145,14 @@ async function handleSave() {
|
||||
config_key: 'miniprogram_empty_image',
|
||||
config_value: miniprogramEmptyImage.value,
|
||||
},
|
||||
{
|
||||
config_key: 'logistics_show_warehouse_name_user',
|
||||
config_value: logisticsShowWhNameUser.value ? '1' : '0',
|
||||
},
|
||||
{
|
||||
config_key: 'logistics_show_warehouse_name_clinic',
|
||||
config_value: logisticsShowWhNameClinic.value ? '1' : '0',
|
||||
},
|
||||
]);
|
||||
message.success('保存成功');
|
||||
} finally {
|
||||
@@ -188,6 +216,33 @@ onMounted(() => {
|
||||
<FormAvatar v-model:value="miniprogramEmptyImage" />
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane key="logistics" tab="物流展示">
|
||||
<div class="py-4 space-y-6">
|
||||
<div>
|
||||
<div class="mb-2 font-medium">患者端:物流多包裹是否显示配送仓名称</div>
|
||||
<div class="mb-2 text-sm text-gray-500">
|
||||
关闭后患者端仅显示「包裹1」「包裹2」,不展示仓名
|
||||
</div>
|
||||
<Switch
|
||||
v-model:checked="logisticsShowWhNameUser"
|
||||
checked-children="显示"
|
||||
un-checked-children="隐藏"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-2 font-medium">诊所端:物流多包裹是否显示配送仓名称</div>
|
||||
<div class="mb-2 text-sm text-gray-500">
|
||||
关闭后诊所/商家小程序订单物流仅显示包裹序号;平台后台产品订单不受影响
|
||||
</div>
|
||||
<Switch
|
||||
v-model:checked="logisticsShowWhNameClinic"
|
||||
checked-children="显示"
|
||||
un-checked-children="隐藏"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
|
||||
<div class="mt-4">
|
||||
|
||||
Reference in New Issue
Block a user