diff --git a/apps/web-antd/src/components/drug-search-select/drug-search-select.vue b/apps/web-antd/src/components/drug-search-select/drug-search-select.vue index 77498c48..5b133505 100644 --- a/apps/web-antd/src/components/drug-search-select/drug-search-select.vue +++ b/apps/web-antd/src/components/drug-search-select/drug-search-select.vue @@ -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(
无图
- +
-
{{ item._drugName }}
+
+
{{ item._drugName }}
+ 多仓 +
ID:{{ item._drugId }} @@ -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 { diff --git a/apps/web-antd/src/store/prescription.ts b/apps/web-antd/src/store/prescription.ts index a9baa635..fa1bcb2e 100644 --- a/apps/web-antd/src/store/prescription.ts +++ b/apps/web-antd/src/store/prescription.ts @@ -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, diff --git a/apps/web-antd/src/utils/formatStoreNameWithHu.ts b/apps/web-antd/src/utils/formatStoreNameWithHu.ts new file mode 100644 index 00000000..64682947 --- /dev/null +++ b/apps/web-antd/src/utils/formatStoreNameWithHu.ts @@ -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; +} diff --git a/apps/web-antd/src/views/business/chat/components/PrescriptionModal.vue b/apps/web-antd/src/views/business/chat/components/PrescriptionModal.vue index be80c5b0..a5b1d218 100644 --- a/apps/web-antd/src/views/business/chat/components/PrescriptionModal.vue +++ b/apps/web-antd/src/views/business/chat/components/PrescriptionModal.vue @@ -224,50 +224,103 @@ const [WarehouseSelectModals, warehouseSelectModalApi] = useVbenModal({ }); /** - * 药店在线问诊:若药品有配送仓绑定,则先弹选仓再发送 + * 在线复诊发送前:有仓药绑定时弹卡片选仓(默认最低价) + * @returns true=已拦截发送等待选仓;false=无需选仓可继续发送 */ const tryPharmacyWarehouseSelect = async ( doctorSecondSignValue: number, sendMode: number, customStoreId: number | null, ): Promise => { - 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 = {}; + 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 = {}; - 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; + 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) => { @@ -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, diff --git a/apps/web-antd/src/views/business/chat/components/WarehouseSelectModal.vue b/apps/web-antd/src/views/business/chat/components/WarehouseSelectModal.vue index 8509d832..1d7e24ee 100644 --- a/apps/web-antd/src/views/business/chat/components/WarehouseSelectModal.vue +++ b/apps/web-antd/src/views/business/chat/components/WarehouseSelectModal.vue @@ -1,92 +1,210 @@ diff --git a/apps/web-antd/src/views/business/order/product-order/api/index.ts b/apps/web-antd/src/views/business/order/product-order/api/index.ts index fa988da8..b6ea9908 100644 --- a/apps/web-antd/src/views/business/order/product-order/api/index.ts +++ b/apps/web-antd/src/views/business/order/product-order/api/index.ts @@ -79,6 +79,26 @@ export async function expressDetailByOrderId(data: Record) { return requestClient.post(`express-detail/detail-by-order`, data); } +/** + * 超管:将历史订单级运单同步到分包裹 shipment + */ +export async function syncLegacyShipmentApi(orderId: number) { + return requestClient.post(`${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(`${prefix}change-delivery-warehouse`, data); +} + /** * 导出订单数据(后端 Excel,旧接口保留) */ diff --git a/apps/web-antd/src/views/business/order/product-order/components/detail.vue b/apps/web-antd/src/views/business/order/product-order/components/detail.vue index 0b4b01ec..0ef71904 100644 --- a/apps/web-antd/src/views/business/order/product-order/components/detail.vue +++ b/apps/web-antd/src/views/business/order/product-order/components/detail.vue @@ -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>(); + const modalData = modalApi.getData>() || {}; + 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 void)>(null); + +/** + * Tab 文案:有仓名才拼括号;空串不兜底「平台/仓」 + */ +function packageTabLabel(pkg: Record, 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() {
-
-
-

物流信息

- - - {{ expressDetail.express_company_name }} - - - {{ expressDetail.express_no }} - - - {{ expressDetail.state_txt }} - - -
-
-

物流追踪

- - +
+

物流信息

+ + + +
+ + + +
+ +
+ +
+ 本包裹尚未发货 + + ({{ pkg.items.map((p: any) => p.drug_name || p.name).filter(Boolean).join('、') }}) + +
+
+
+ + + {{ expressDetail.express_company_name || '-' }} + + + {{ expressDetail.express_no || '-' }} + + + {{ expressDetail.state_txt || '-' }} + +
+ diff --git a/apps/web-antd/src/views/business/order/product-order/components/modal.vue b/apps/web-antd/src/views/business/order/product-order/components/modal.vue index 2edb369e..6e717717 100644 --- a/apps/web-antd/src/views/business/order/product-order/components/modal.vue +++ b/apps/web-antd/src/views/business/order/product-order/components/modal.vue @@ -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 = { ...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>(); + const { values, update } = modalApi.getData>() || {}; + // 先重置,避免上次发货残留的 warehouse_id + formApi.resetForm(); if (values) { orderNo.value = values.order_no; isUpdate.value = update; diff --git a/apps/web-antd/src/views/business/order/product-order/config/form.ts b/apps/web-antd/src/views/business/order/product-order/config/form.ts index 0361eece..a4da106c 100644 --- a/apps/web-antd/src/views/business/order/product-order/config/form.ts +++ b/apps/web-antd/src/views/business/order/product-order/config/form.ts @@ -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: { diff --git a/apps/web-antd/src/views/business/order/product-order/index.vue b/apps/web-antd/src/views/business/order/product-order/index.vue index 8ccb6042..94b403fa 100644 --- a/apps/web-antd/src/views/business/order/product-order/index.vue +++ b/apps/web-antd/src/views/business/order/product-order/index.vue @@ -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 = {}) => { 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 = {}) => { + logisticsModalApi.setData({ + order_id: data?.id, + }); + logisticsModalApi.open(); +}; + +/** + * 打开发货弹窗。 + * 列表不传 warehouse_id(后端平台默认 0);详情按包发时仅传合法非负整数。 + * 注意:勿用 bind(null,row),table-action 的 click 会把事件当成第 2 参污染 warehouse_id。 + */ +const wareSend = (data: Record = {}, warehouseId?: number) => { + const values: Record = { + 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) { } } +/** 订单页改仓弹窗状态 */ +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(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 | 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, + 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 = () => { + {
{{ formatExpressAddress(row) }}
- + + + +
+ 当前仓库:{{ changeWhFromName || '—' }} +
+
+ 加载可选仓库… +
+
+ +
+ 暂无其它可用配送仓库 +
+
+