feat: 医生创建就诊人发送前确认收货地址,成功页与处方记录可看订单

发送前可选填/一键诊所地址;开方成功展示收货信息;处方记录支持查看订单详情。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
李琦
2026-10-20 10:02:40 +08:00
parent 618abca975
commit 597a397f68
3 changed files with 359 additions and 7 deletions

View File

@@ -0,0 +1,216 @@
<script lang="ts" setup>
/**
* 医生创建就诊人:发送处方前确认收货地址
* 地址选填;无地址可一键填诊所或打开编辑;确认后继续发送
*/
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button, Spin, message } from 'ant-design-vue';
import { formatShipAddressCopyText } from '#/util/ship-address-copy';
import {
getCreatedUserPatientDetailApi,
getCurrentStoreTypeApi,
updateCreatedUserPatientApi,
} from '#/views/doctor/doctor-reception/api';
import EditCreatedUserPatientModal from './EditCreatedUserPatientModal.vue';
export type ShipAddressSnapshot = {
name?: string;
mobile?: string;
province?: string;
city?: string;
area?: string;
region?: string;
detail_address?: string;
path_label?: string;
};
type ClinicAddress = {
available: boolean;
city: string;
city_id: number;
contact: string;
mobile: string;
path_label: string;
position: string;
province: string;
province_id: number;
};
const loading = ref(false);
const applyingClinic = ref(false);
const userPatientId = ref(0);
const patientName = ref('');
const shipAddress = ref<ShipAddressSnapshot>({});
const clinicAddress = ref<ClinicAddress | null>(null);
const onConfirmCb = ref<((ship: ShipAddressSnapshot) => void) | null>(null);
const shipText = computed(() =>
formatShipAddressCopyText(shipAddress.value || {}),
);
const hasShip = computed(() => !!shipText.value);
const clinicAddressText = computed(() => {
const a = clinicAddress.value;
if (!a?.available) return '';
return `${a.path_label || ''}${a.position || ''}`;
});
const [EditShipModal, editShipModalApi] = useVbenModal({
connectedComponent: EditCreatedUserPatientModal,
});
/** 拉取就诊人地址与诊所地址 */
async function loadData() {
if (!userPatientId.value) return;
loading.value = true;
try {
const [detail, storeRes] = await Promise.all([
getCreatedUserPatientDetailApi(userPatientId.value),
getCurrentStoreTypeApi().catch(() => null),
]);
shipAddress.value = (detail?.ship_address || {}) as ShipAddressSnapshot;
clinicAddress.value = (storeRes?.store_address ||
null) as ClinicAddress | null;
} catch (error: any) {
message.error(error?.message || '加载收货地址失败');
} finally {
loading.value = false;
}
}
/** 一键写入诊所地址到就诊人(选填场景的快捷填充) */
async function applyClinicAddress() {
const a = clinicAddress.value;
if (!a?.available) {
message.warning('当前诊所未配置完整地址');
return;
}
if (!userPatientId.value) return;
applyingClinic.value = true;
try {
const detail = await getCreatedUserPatientDetailApi(userPatientId.value);
// full 模式后端会写 name/sex 等;必须带回详情原值,避免只传 ship 时把姓名刷空
const payload: Record<string, any> = {
user_patient_id: userPatientId.value,
mobile: String(detail?.mobile || '').trim(),
ship_name: String(a.contact || detail?.name || '').slice(0, 10),
ship_mobile: String(a.mobile || detail?.mobile || '').trim(),
ship_province: a.province || '',
ship_city: a.city || '',
ship_area: '',
ship_region: a.path_label || `${a.province || ''}${a.city || ''}`,
ship_detail_address: a.position || '',
};
if (String(detail?.edit_mode || '') !== 'limited') {
payload.name = String(detail?.name || '').trim();
payload.id_card = String(detail?.id_card || '').trim();
payload.birthday = String(detail?.birthday || '').trim();
payload.sex = Number(detail?.sex || 1);
payload.age = detail?.age;
}
await updateCreatedUserPatientApi(payload);
message.success('已填入诊所地址');
await loadData();
} catch (error: any) {
message.error(error?.message || '填入诊所地址失败');
} finally {
applyingClinic.value = false;
}
}
/** 打开编辑弹窗手填/粘贴修改地址 */
function openEditShip() {
editShipModalApi.setData({
user_patient_id: userPatientId.value,
onSaved: () => {
void loadData();
},
});
editShipModalApi.open();
}
function confirmSend() {
onConfirmCb.value?.(shipAddress.value || {});
modalApi.close();
}
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
title: '确认收货地址',
confirmText: '确认发送',
cancelText: '取消',
contentClass: 'max-h-[70vh]',
onConfirm: () => {
confirmSend();
},
onOpenChange(isOpen: boolean) {
if (!isOpen) return;
const data = modalApi.getData() as {
user_patient_id?: number;
patient_name?: string;
onConfirm?: (ship: ShipAddressSnapshot) => void;
};
userPatientId.value = Number(data?.user_patient_id || 0);
patientName.value = String(data?.patient_name || '');
onConfirmCb.value = data?.onConfirm || null;
shipAddress.value = {};
clinicAddress.value = null;
void loadData();
},
});
</script>
<template>
<Modal class="w-[560px] max-w-[95vw]">
<EditShipModal />
<Spin :spinning="loading">
<p class="mb-3 text-sm text-[hsl(var(--muted-foreground))]">
就诊人
<b class="text-[hsl(var(--foreground))]">{{ patientName || '—' }}</b>
由医生创建,请确认收货地址后发送。地址选填,可不填直接发送。
</p>
<div
v-if="hasShip"
class="rounded border border-[hsl(var(--primary)/30%)] px-3 py-2 shadow-[0_0_6px_hsl(var(--primary)/18%)]"
>
<div class="mb-1 text-xs text-[hsl(var(--muted-foreground))]">
当前收货地址
</div>
<div class="break-all text-sm text-[hsl(var(--foreground))]">
{{ shipText }}
</div>
</div>
<div
v-else
class="rounded border border-dashed border-[hsl(var(--border))] bg-[hsl(var(--muted)/0.25)] px-3 py-3 text-sm text-[hsl(var(--muted-foreground))]"
>
暂无收货地址(选填)。可一键填入诊所地址,或手动填写后再发送。
</div>
<div
v-if="clinicAddress?.available"
class="mt-3 text-xs text-[hsl(var(--muted-foreground))]"
>
诊所地址:{{ clinicAddressText }}
</div>
<div class="mt-4 flex flex-wrap gap-2">
<Button
v-if="clinicAddress?.available"
type="primary"
ghost
:loading="applyingClinic"
@click="applyClinicAddress"
>
使用诊所地址
</Button>
<Button @click="openEditShip">
{{ hasShip ? '修改地址' : '填写地址' }}
</Button>
</div>
</Spin>
</Modal>
</template>

View File

@@ -33,6 +33,8 @@ type DrawerData = {
onReuse?: (prescriptionId: number) => void | Promise<void>;
/** 点击处方号打开详情 */
onDetail?: (prescriptionId: number) => void;
/** 查看商品订单详情(有 order_id 时) */
onOrderDetail?: (orderId: number) => void;
/** 是否允许撤回(线下接诊) */
allowWithdraw?: boolean;
/** 撤回成功后刷新 */
@@ -178,6 +180,20 @@ function handleOpenDetail(row: Record<string, any>) {
message.warning('暂无法打开处方详情');
}
/** 打开商品订单详情(父级挂载 OrderDetail 弹窗) */
function handleOpenOrderDetail(row: Record<string, any>) {
const orderId = Number(row.order_id || 0);
if (!orderId) {
message.warning('暂无关联订单');
return;
}
if (typeof meta.value.onOrderDetail === 'function') {
meta.value.onOrderDetail(orderId);
return;
}
message.warning('暂无法打开订单详情');
}
async function handleWithdraw(row: Record<string, any>) {
const prescriptionId = Number(row.id || 0);
const registerId = Number(row.register_id || meta.value.registerId || 0);
@@ -302,8 +318,16 @@ defineExpose({ drawerApi });
<div class="rx-history-meta text-muted-foreground">
<span>开方时间:{{ row.created_at || '—' }}</span>
<span class="ml-3">金额:¥{{ row.total_pay_price ?? '—' }}</span>
<span v-if="row.order_no" class="ml-3">订单号:{{ row.order_no }}</span>
<span v-if="row.order_id" class="ml-3">订单ID:{{ row.order_id }}</span>
<span v-if="row.order_no" class="ml-3">
订单号:
<a
class="cursor-pointer text-primary hover:underline"
title="查看订单详情"
@click.prevent="handleOpenOrderDetail(row)"
>
{{ row.order_no }}
</a>
</span>
<span
v-if="listTab === 'history' && row.register?.order_no"
class="ml-3"
@@ -313,6 +337,14 @@ defineExpose({ drawerApi });
</div>
</div>
<div class="rx-history-actions">
<Button
v-if="row.order_id"
type="link"
size="small"
@click="handleOpenOrderDetail(row)"
>
查看订单
</Button>
<Button
type="link"
size="small"

View File

@@ -72,6 +72,7 @@ import { DrugSearchSelect } from '#/components/drug-search-select';
import InfoModal from '#/components/modal/InfoModal.vue';
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
import { useOrderPriceAdjust } from '#/composables/useOrderPriceAdjust';
import { formatShipAddressCopyText } from '#/util/ship-address-copy';
import { getRegisterStatus } from '#/util/tool';
import {
calcItemMarginPercent,
@@ -150,6 +151,11 @@ import {
saveChineseCommonPrescriptionApi,
saveWestCommonPrescriptionApi,
} from '#/views/doctor/settings/api';
import OrderDetail from '#/views/business/order/product-order/components/detail.vue';
import {
openOrderPopup,
ORDER_POPUP_Z_INDEX,
} from '#/views/business/order/utils/popup-z-index';
import { applyPublicAccountPay } from '#/views/finance/public-account-pay/api';
import ApplyPublicAccountPayModal from '#/views/finance/public-account-pay/components/apply-modal.vue';
@@ -160,6 +166,7 @@ import ClaimQrcodeModal from './components/ClaimQrcodeModal.vue';
import CommonPrescriptionModal from './components/CommonPrescriptionModal.vue';
// 确认模态框组件
import ConfirmModal from './components/ConfirmModal.vue';
import ConfirmShipAddressBeforeSendModal from './components/ConfirmShipAddressBeforeSendModal.vue';
import CreateUserPatientModal from './components/CreateUserPatientModal.vue';
import DoctorCreatedPatientModal from './components/DoctorCreatedPatientModal.vue';
import GoldenFormulaModal from './components/GoldenFormulaModal.vue';
@@ -1553,8 +1560,11 @@ const checkChineseMedicineConflict = () => {
});
return;
};
/** 发送前确认弹窗带回的收货快照,成功页展示用 */
const pendingShipForSuccess = ref<Record<string, any> | null>(null);
/**
* 发送处方
* 发送处方:校验通过后,医生创建就诊人先确认收货地址(选填 / 可一键诊所地址)
*/
const sendPrescription = () => {
if (currentDrugs.value.length === 0) {
@@ -1585,6 +1595,29 @@ const sendPrescription = () => {
message.error('医嘱不能为空');
return;
}
const up = activePatient.value as any;
const upId = Number(up?.id || 0);
const isDoctorCreated = Number(up?.created_by_doctor_id || 0) > 0;
if (isDoctorCreated && upId > 0) {
ConfirmShipAddressBeforeSendModalApi.setData({
user_patient_id: upId,
patient_name: String(up?.name || ''),
onConfirm: (ship: Record<string, any>) => {
pendingShipForSuccess.value = ship || {};
doSendPrescription();
},
});
ConfirmShipAddressBeforeSendModalApi.open();
return;
}
pendingShipForSuccess.value = null;
doSendPrescription();
};
/**
* 真正调用开方接口(确认地址后或非医生创建就诊人直接走这里)
*/
const doSendPrescription = () => {
// 发送处方逻辑
addWestPrescription({
// 当前活动患者的个人信息
@@ -1647,6 +1680,9 @@ const sendPrescription = () => {
message.success('处方已发送');
// 处方已提交,重置 AI 关联,避免下一张处方误关联
aiRxGenerationId.value = 0;
const shipText = formatShipAddressCopyText(
pendingShipForSuccess.value || {},
);
// 先记录本次发送结果(下面会清空诊断/购物车,成功页要用的数据在这里先落住)
sentPrescriptionInfo.value = {
order_id: Number(res?.order_id || 0),
@@ -1655,7 +1691,11 @@ const sendPrescription = () => {
is_pay: Number(res?.is_pay || 0),
patient_name: activePatient.value?.name || '',
diagnosis: diagnosis.value,
// 医生创建就诊人发送前确认过的收货地址(选填时可为空)
ship_address_text: shipText,
has_ship_confirm: pendingShipForSuccess.value !== null,
};
pendingShipForSuccess.value = null;
// 有 VIP 时同步保存病历(诊断/医嘱已与处方同源)
if (canUseMedicalRecord.value) {
try {
@@ -1818,15 +1858,44 @@ const [DoctorCreatedPatientModals, DoctorCreatedPatientModalApi] = useVbenModal(
},
);
/** 医生创建就诊人:发送前确认收货地址(选填 / 一键诊所地址) */
const [
ConfirmShipAddressBeforeSendModals,
ConfirmShipAddressBeforeSendModalApi,
] = useVbenModal({
connectedComponent: ConfirmShipAddressBeforeSendModal,
});
/** 医生代支付码弹窗 */
const [ProxyPayQrcodeModals, ProxyPayQrcodeModalApi] = useVbenModal({
connectedComponent: ProxyPayQrcodeModal,
});
/** 商品订单详情(开方成功 / 处方记录入口,复用订单管理详情弹窗) */
const [ProductOrderDetailModal, ProductOrderDetailModalApi] = useVbenModal({
connectedComponent: OrderDetail,
});
const [ApplyPublicAccountPayModals, ApplyPublicAccountPayModalApi] =
useVbenModal({
connectedComponent: ApplyPublicAccountPayModal,
});
/**
* 打开商品订单详情
* 为什么用 NESTED:处方记录是抽屉,详情需盖在抽屉之上
*/
function openProductOrderDetail(orderId: number) {
const id = Number(orderId || 0);
if (!id) {
message.warning('暂无关联订单');
return;
}
openOrderPopup(
ProductOrderDetailModalApi,
{ id },
ORDER_POPUP_Z_INDEX.NESTED,
);
}
// ==================== 常用方弹窗 ====================
const [CommonPrescriptionModals, CommonPrescriptionModalApi] = useVbenModal({
connectedComponent: CommonPrescriptionModal,
@@ -3354,6 +3423,8 @@ async function openPatientRxHistoryDrawer() {
// 点击处方号打开详情(与医生名片/患者管理一致)
onDetail: (prescriptionId: number) =>
openPrescriptionDetail(prescriptionId),
// 查看关联商品订单详情
onOrderDetail: (orderId: number) => openProductOrderDetail(orderId),
publicAccountPayEnabled: publicAccountPayEnabled.value,
publicAccountPayTrigger: publicAccountPayTrigger.value,
publicAccountBillMode: publicAccountBillMode.value,
@@ -4254,7 +4325,9 @@ function onStoreSelectOpenChange(open: boolean) {
<CreateUserPatientModals />
<ClaimQrcodeModals />
<DoctorCreatedPatientModals />
<ConfirmShipAddressBeforeSendModals />
<ProxyPayQrcodeModals />
<ProductOrderDetailModal />
<ApplyPublicAccountPayModals />
<InfoModalComponent />
<!-- 左侧患者列表:悬停展开、移出收起;收起态仅箭头+背景提示 -->
@@ -5761,13 +5834,25 @@ function onStoreSelectOpenChange(open: boolean) {
<h3 class="rx-sent-title">处方已发送</h3>
<p class="rx-sent-sub">已推送到患者微信 · 支付完成后门店按方发药</p>
<div class="rx-sent-pills">
<Button
v-if="sentPrescriptionInfo?.order_id"
shape="round"
size="small"
type="primary"
ghost
@click="
openProductOrderDetail(Number(sentPrescriptionInfo.order_id))
"
>
查看订单
</Button>
<Button
shape="round"
size="small"
@click="openPatientRxHistoryDrawer"
>
处方记录
</Button>
>
处方记录
</Button>
<Button
v-if="canEndReception"
shape="round"
@@ -5784,12 +5869,31 @@ function onStoreSelectOpenChange(open: boolean) {
<div class="rx-sent-card rx-sent-detail">
<div class="rx-sent-row">
<span>订单号</span>
<b>{{ sentPrescriptionInfo?.order_no || '—' }}</b>
<a
v-if="sentPrescriptionInfo?.order_id"
class="cursor-pointer font-medium text-primary hover:underline"
title="查看订单详情"
@click.prevent="
openProductOrderDetail(Number(sentPrescriptionInfo.order_id))
"
>
{{ sentPrescriptionInfo?.order_no || '—' }}
</a>
<b v-else>{{ sentPrescriptionInfo?.order_no || '—' }}</b>
</div>
<div class="rx-sent-row">
<span>患者</span>
<b>{{ sentPrescriptionInfo?.patient_name || '—' }}</b>
</div>
<div
v-if="sentPrescriptionInfo?.has_ship_confirm"
class="rx-sent-row"
>
<span>收货地址</span>
<b class="break-all text-right">{{
sentPrescriptionInfo?.ship_address_text || '未填写'
}}</b>
</div>
<div v-if="sentPrescriptionInfo?.diagnosis" class="rx-sent-row">
<span>诊断</span>
<b>{{ sentPrescriptionInfo.diagnosis }}</b>