fix: 修复就诊人地址落库,并支持订单未同步ERP时改收货

选择并接诊时可预览确认导入本次地址;粘贴识别改为长到短取名,避免「罗朝亮」被截成「朝亮」。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
李琦
2026-10-20 09:39:19 +08:00
parent ec5578d4e4
commit cd216222d3
7 changed files with 663 additions and 41 deletions

View File

@@ -387,7 +387,8 @@ export class AddressRecognizer {
const m = after.match(/^\s*[,,、]?\s*([\u4E00-\u9FA5]{2,6})/);
if (!m) return null;
const chunk = m[1];
for (let len = 2; len <= chunk.length; len++) {
// 长到短:与手机号前取名同口径,避免短姓误截
for (let len = chunk.length; len >= 2; len--) {
const cand = chunk.slice(0, len);
if (this.isValidName(cand)) {
return { value: cand, confidence: len <= 3 ? 0.8 : 0.7 };
@@ -397,7 +398,8 @@ export class AddressRecognizer {
}
/**
* 从手机号前取名:短到长尝试,避免「张三杭州」整段误判;支持复姓最长 6 字
* 从手机号前取名:长到短优先,避免姓氏表含生僻单姓(如「朝」)时把「罗朝亮」截成「朝亮」
* 支持复姓最长 6 字
*/
_nameBeforePhone(text, phoneInfo) {
if (!phoneInfo || !phoneInfo.value) return null;
@@ -407,7 +409,7 @@ export class AddressRecognizer {
const m = before.match(/([\u4E00-\u9FA5]{2,6})\s*$/);
if (!m) return null;
const chunk = m[1];
for (let len = 2; len <= chunk.length; len++) {
for (let len = chunk.length; len >= 2; len--) {
const cand = chunk.slice(chunk.length - len);
if (this.isValidName(cand)) {
return { value: cand, confidence: len <= 3 ? 0.85 : 0.75 };
@@ -770,8 +772,9 @@ export class AddressRecognizer {
normalizeEcommerceText(text) {
const src = String(text || '');
if (src.indexOf('\n') < 0 && src.indexOf('\r') < 0) return src;
// 保留标签名:展平后仍能走「收货人:xxx」关键词取名,避免只剩裸姓名被短姓截断
const labelRe =
/^(?:收货人|收件人|联系人|姓名|手机号码|手机号|手机|联系电话|联系方式|电话|所在地区|详细地址|地址)\s*[::]\s*(.+)$/;
/^(收货人|收件人|联系人|姓名|手机号码|手机号|手机|联系电话|联系方式|电话|所在地区|详细地址|地址)\s*[::]\s*(.+)$/;
const lines = src.split(/\r?\n/);
const parts = [];
let hitLabel = false;
@@ -779,9 +782,9 @@ export class AddressRecognizer {
const line = String(lines[i] || '').trim();
if (!line) continue;
const m = line.match(labelRe);
if (m && m[1]) {
if (m && m[2]) {
hitLabel = true;
parts.push(m[1].trim());
parts.push(`${m[1]}:${String(m[2]).trim()}`);
} else {
parts.push(line);
}

View File

@@ -120,6 +120,30 @@ export async function getStoreShipAddress(orderId: number) {
});
}
/**
* 单独修改收货信息(不发货;后端校验未同步 ERP)
*/
export async function updateShipAddressApi(data: {
order_id: number;
express_name?: string;
express_mobile?: string;
express_region?: string;
express_address?: string;
province?: string;
city?: string;
area?: string;
}) {
return requestClient.post<{
order_id: number;
order_no: string;
delivery_method: number;
express_name: string;
express_mobile: string;
express_region: string;
express_address: string;
}>(`${prefix}update-ship-address`, data);
}
/**
* 订单物流追踪
* @param data

View File

@@ -0,0 +1,295 @@
<script lang="ts" setup>
/**
* 商品订单单独修改收货信息(发货前独立通道)
* 未同步 ERP 可改;支持粘贴识别;可一键填入诊所地址
*/
import type { AddressRecognizeResult } from '#/components/address-paste-block/AddressPasteBlock.vue';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button, Input, message } from 'ant-design-vue';
import AddressPasteBlock from '#/components/address-paste-block/AddressPasteBlock.vue';
import {
getShipReceiver,
getStoreShipAddress,
updateShipAddressApi,
} from '#/views/business/order/product-order/api';
const gridApi = ref();
const orderId = ref(0);
const orderNo = ref('');
const loading = ref(false);
const storeAddrLoading = ref(false);
/** 粘贴识别拆出的省市区,提交时写入 address 快照 */
const regionParts = ref({ province: '', city: '', area: '' });
const form = ref({
express_name: '',
express_mobile: '',
express_region: '',
express_address: '',
});
const pasteCurrentValues = computed(() => ({
name: String(form.value.express_name || '').trim(),
phone: String(form.value.express_mobile || '').trim(),
region: String(form.value.express_region || '').trim(),
detail: String(form.value.express_address || '').trim(),
}));
const pasteFilled = computed(() => ({
name: !!pasteCurrentValues.value.name,
phone: !!pasteCurrentValues.value.phone,
region: !!pasteCurrentValues.value.region,
detail: !!pasteCurrentValues.value.detail,
}));
/** 规范化收件四字段 */
function normalizeReceiver(next: {
express_address?: string;
express_mobile?: string;
express_name?: string;
express_region?: string;
}) {
return {
express_name: String(next.express_name || ''),
express_mobile: String(next.express_mobile || ''),
express_region: String(next.express_region || ''),
express_address: String(next.express_address || ''),
};
}
/**
* 打开后拉库内最新收件;失败回落列表行字段
*/
async function loadReceiver(fallback?: Record<string, any>) {
if (!orderId.value) return;
loading.value = true;
try {
const res = await getShipReceiver(orderId.value);
form.value = normalizeReceiver(res || {});
} catch (error) {
console.error(error);
if (fallback) {
const addr =
fallback.address && typeof fallback.address === 'object'
? fallback.address
: {};
form.value = normalizeReceiver({
express_name: fallback.express_name || addr.name || '',
express_mobile: fallback.express_mobile || addr.mobile || '',
express_region: fallback.express_region || addr.region || '',
express_address: fallback.express_address || addr.detail_address || '',
});
regionParts.value = {
province: String(addr.province || ''),
city: String(addr.city || ''),
area: String(addr.area || ''),
};
}
} finally {
loading.value = false;
}
}
/**
* 粘贴识别回填:有值才覆盖;省市区拼进 express_region,并缓存拆分字段
*/
function onAddressRecognized(result: AddressRecognizeResult) {
if (!result) return;
if (result.name) {
form.value.express_name = String(result.name).slice(0, 50);
}
if (result.phone) {
form.value.express_mobile = result.phone;
}
if (result.detail) {
form.value.express_address = result.detail;
}
if (result.province || result.city || result.area) {
regionParts.value = {
province: String(result.province || '').trim(),
city: String(result.city || '').trim(),
area: String(result.area || '').trim(),
};
form.value.express_region = [
regionParts.value.province,
regionParts.value.city,
regionParts.value.area,
]
.filter(Boolean)
.join(' ');
}
}
/** 一键填入订单所属诊所地址 */
async function fillStoreAddress() {
if (!orderId.value) {
message.warning('订单信息缺失');
return;
}
storeAddrLoading.value = true;
try {
const res = await getStoreShipAddress(orderId.value);
form.value = normalizeReceiver(res || {});
regionParts.value = { province: '', city: '', area: '' };
message.success('已填入诊所地址');
} catch (error) {
console.error(error);
} finally {
storeAddrLoading.value = false;
}
}
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
confirmText: '保存',
onConfirm: async () => {
const mobile = String(form.value.express_mobile || '').trim();
if (!mobile) {
message.warning('请填写收件手机号');
return;
}
if (!/^\d{11}$/.test(mobile)) {
message.warning('请填写正确的11位手机号');
return;
}
modalApi.setState({ loading: true, confirmLoading: true });
try {
await updateShipAddressApi({
order_id: orderId.value,
express_name: String(form.value.express_name || '').trim(),
express_mobile: mobile,
express_region: String(form.value.express_region || '').trim(),
express_address: String(form.value.express_address || '').trim(),
province: regionParts.value.province,
city: regionParts.value.city,
area: regionParts.value.area,
});
message.success('收货信息已更新');
gridApi.value?.reload?.();
modalApi.close();
} catch (error) {
console.error(error);
} finally {
modalApi.setState({ loading: false, confirmLoading: false });
}
},
onOpenChange(isOpen: boolean) {
if (!isOpen) return;
const data = modalApi.getData<Record<string, any>>() || {};
gridApi.value = data.gridApi || null;
const values = data.values || {};
orderId.value = Number(values.order_id || values.id || 0);
orderNo.value = String(values.order_no || '');
regionParts.value = { province: '', city: '', area: '' };
form.value = normalizeReceiver(values);
void loadReceiver(values);
},
});
</script>
<template>
<Modal
:title="`修改收货信息${orderNo ? ` · ${orderNo}` : ''}`"
class="w-[560px] max-w-[95vw]"
>
<div class="ship-edit">
<div class="ship-edit__toolbar">
<Button
type="link"
size="small"
class="!px-0"
:loading="storeAddrLoading"
:disabled="loading"
@click="fillStoreAddress"
>
用诊所地址
</Button>
<span v-if="loading" class="ship-edit__loading">加载中…</span>
</div>
<AddressPasteBlock
:filled="pasteFilled"
:current-values="pasteCurrentValues"
@recognized="onAddressRecognized"
/>
<div class="ship-edit__field">
<label class="ship-edit__label">收件人</label>
<Input
v-model:value="form.express_name"
placeholder="选填"
:maxlength="50"
allow-clear
/>
</div>
<div class="ship-edit__field">
<label class="ship-edit__label">手机号</label>
<Input
v-model:value="form.express_mobile"
placeholder="必填,11 位手机号"
:maxlength="11"
allow-clear
/>
</div>
<div class="ship-edit__field">
<label class="ship-edit__label">所在地区</label>
<Input
v-model:value="form.express_region"
placeholder="省市区,可粘贴识别自动填入"
:maxlength="100"
allow-clear
/>
</div>
<div class="ship-edit__field">
<label class="ship-edit__label">详细地址</label>
<Input.TextArea
v-model:value="form.express_address"
placeholder="街道门牌等"
:rows="3"
:maxlength="255"
show-count
/>
</div>
<p class="ship-edit__hint">
仅未同步 ERP 的订单可改;保存后写回订单收件信息,不触发发货
</p>
</div>
</Modal>
</template>
<style scoped>
.ship-edit {
display: flex;
flex-direction: column;
gap: 12px;
}
.ship-edit__toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.ship-edit__loading {
font-size: 12px;
color: hsl(var(--muted-foreground));
}
.ship-edit__field {
display: flex;
flex-direction: column;
gap: 6px;
}
.ship-edit__label {
font-size: 13px;
color: hsl(var(--foreground));
}
.ship-edit__hint {
margin: 0;
font-size: 12px;
line-height: 1.5;
color: hsl(var(--muted-foreground));
}
</style>

View File

@@ -296,7 +296,12 @@ const [Modal, modalApi] = useVbenModal({
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
if (isOpen) {
const modalData = modalApi.getData<Record<string, any>>() || {};
const { values, id, onShipPackage: shipFn } = modalData;
const {
values,
id,
onShipPackage: shipFn,
onEditShipAddress: editShipFn,
} = modalData;
fromTrace.value = !!modalData.fromTrace;
onOpenTraceFn.value =
typeof modalData.onOpenTrace === 'function'
@@ -308,6 +313,8 @@ const [Modal, modalApi] = useVbenModal({
: null;
// 列表传入:按 warehouse_id 打开发货弹窗(平台包=0)
onShipPackage.value = typeof shipFn === 'function' ? shipFn : null;
onEditShipAddress.value =
typeof editShipFn === 'function' ? editShipFn : null;
if (id) {
getOrderInfo(id).then((res) => {
data.value = res;
@@ -322,6 +329,7 @@ const [Modal, modalApi] = useVbenModal({
}
} else {
onShipPackage.value = null;
onEditShipAddress.value = null;
fromTrace.value = false;
onOpenTraceFn.value = null;
onOpenPrescriptionFn.value = null;
@@ -352,6 +360,18 @@ const packageTabs = computed(() => {
const activePkg = ref('0');
/** 详情内「发本包」回调(由列表传入时仅平台包裹可发) */
const onShipPackage = ref<((warehouseId: number) => void) | null>(null);
/** 详情内打开「修改收货信息」(未同步 ERP) */
const onEditShipAddress = ref<((row: Record<string, any>) => void) | null>(
null,
);
/** 快递且未同步 ERP 时可改收货 */
const canEditShipAddress = computed(
() =>
deliveryMethod.value === 0 &&
data.value &&
Number(data.value.is_sync_erp) !== 1 &&
typeof onEditShipAddress.value === 'function',
);
/**
* Tab 文案:有仓名才拼括号;空串不兜底「平台/仓」
@@ -614,15 +634,25 @@ function prescriptionStatusColor() {
<h3 class="m-0">
{{ deliveryMethod === 0 ? '收货人' : '就诊人' }}信息
</h3>
<Button
v-if="deliveryMethod === 0"
type="link"
size="small"
class="px-0"
@click="copyOrderShipAddress"
>
一键复制
</Button>
<Space v-if="deliveryMethod === 0" :size="4">
<Button
v-if="canEditShipAddress"
type="link"
size="small"
class="px-0"
@click="onEditShipAddress?.(data)"
>
修改收货
</Button>
<Button
type="link"
size="small"
class="px-0"
@click="copyOrderShipAddress"
>
一键复制
</Button>
</Space>
</div>
<Descriptions
:column="{ xxl: 2, xl: 2, lg: 2, md: 1, sm: 1, xs: 1 }"

View File

@@ -83,6 +83,7 @@ import { isPlatformSuperAdmin } from '#/views/system/admin/_shared/platform-admi
import OrderUserInfoCell from './components/cells/OrderUserInfoCell.vue';
import DetailModal from './components/detail.vue';
import LogisticsModal from './components/logistics-modal.vue';
import EditShipAddressModal from './components/EditShipAddressModal.vue';
import FormModalDemo from './components/modal.vue';
import ProductOrderCardList from './components/ProductOrderCardList.vue';
import ProductOrderExportModal from './components/ProductOrderExportModal.vue';
@@ -365,6 +366,10 @@ onMounted(() => {
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: FormModalDemo,
});
/** 发货前单独改收货信息(未同步 ERP) */
const [EditShipAddressModalComp, editShipAddressModalApi] = useVbenModal({
connectedComponent: EditShipAddressModal,
});
const [Modal, modalApi] = useVbenModal({
connectedComponent: DetailModal,
});
@@ -400,6 +405,11 @@ const infoModal = (data: Record<string, any> = {}) => {
modalApi.close();
wareSend(data, warehouseId);
},
// 详情内改收货:关详情后打开改址弹窗(避免叠层)
onEditShipAddress: (row: Record<string, any>) => {
modalApi.close();
openEditShipAddress(row || data);
},
// 二级弹窗必须页面级挂载,详情里再嵌溯源会被盖住
onOpenTrace: (payload: Record<string, any>) => {
openOrderPopup(traceDrawerApi, payload, ORDER_POPUP_Z_INDEX.NESTED);
@@ -421,6 +431,27 @@ const openLogisticsModal = (data: Record<string, any> = {}) => {
logisticsModalApi.open();
};
/**
* 打开「修改收货信息」弹窗(不发货;仅未同步 ERP)
*/
const openEditShipAddress = (data: Record<string, any> = {}) => {
const addr =
data?.address && typeof data.address === 'object' ? data.address : {};
editShipAddressModalApi.setData({
values: {
order_id: data?.id,
order_no: data?.order_no,
express_name: data?.express_name || addr.name || '',
express_mobile: data?.express_mobile || addr.mobile || '',
express_region: data?.express_region || addr.region || '',
express_address: data?.express_address || addr.detail_address || '',
address: addr,
},
gridApi: gridApiProxy,
});
editShipAddressModalApi.open();
};
/**
* 打开发货弹窗。
* 列表不传 warehouse_id(后端平台默认 0);详情按包发时仅传合法非负整数。
@@ -1328,6 +1359,15 @@ function buildRowActions(row: Record<string, any>): {
(Number(row.status) === 2 || Number(row.status) === 7),
onClick: handleRetryConfirmReceive.bind(null, row),
},
{
// 发货前独立改址通道;已同步 ERP 后端也会拒绝
label: '改收货',
type: 'link',
icon: 'mdi:map-marker-path',
auth: ['Super Admin', 'Admin'],
ifShow: row.is_sync_erp !== 1,
onClick: () => openEditShipAddress(row),
},
{
label: '发货',
type: 'link',
@@ -1362,6 +1402,7 @@ function buildRowActions(row: Record<string, any>): {
<template>
<Page auto-content-height title="订单管理">
<FormModal />
<EditShipAddressModalComp />
<PublicAccountConfirmModals />
<ApplyPublicAccountPayModals />
<Modal />

View File

@@ -39,11 +39,14 @@ import {
formatAddressDisplay,
resolveAddressPairFromNames,
} from '#/util/address-index';
import { formatShipAddressCopyText } from '#/util/ship-address-copy';
import {
checkCreatedUserPatientDuplicateApi,
createUserPatientApi,
getCreatedUserPatientDetailApi,
getCurrentStoreTypeApi,
startOfflineReceptionByPatientApi,
updateCreatedUserPatientApi,
} from '#/views/doctor/doctor-reception/api';
import {
ALLERGIC_HISTORY_OPTIONS,
@@ -96,6 +99,11 @@ const submitting = ref(false);
/** 部分重复时用户确认仍新建 */
const forceCreate = ref(0);
const form = ref(buildEmptyForm());
/**
* 中文省市区兜底:粘贴识别/诊所地址写入;ID 解析失败时仍能提交 ship_*
* 为什么:ensureResolvedAddress 失败只回 ID、无 label,旧逻辑会静默丢掉整段地址
*/
const shipLabels = ref({ province: '', city: '', area: '' });
const onSuccessCb = ref<((res: CreateUserPatientResult) => void) | null>(null);
const onSelectExistingCb = ref<
((res: DoctorCreatedPatientStartResult) => Promise<void> | void) | null
@@ -117,6 +125,15 @@ const dupItems = ref<
}>
>([]);
const dupStartingId = ref(0);
/** 选择并接诊:本次填了地址时,预览确认是否导入到已有就诊人 */
const importConfirmVisible = ref(false);
const importConfirmLoading = ref(false);
const pendingDupItem = ref<{ id: number; mobile?: string; name?: string } | null>(
null,
);
const importHasOld = ref(false);
const importOldText = ref('');
const importNewText = ref('');
/** 识别回填字段高亮 */
const flashMap = ref({
name: false,
@@ -332,6 +349,52 @@ function setHistoryTags(
(form.value[historyField] as string[]) = tags || [];
}
/**
* 组装本次填写的收货 ship_*(有详细地址 + 省市区才返回;解析失败抛错)
* 供创建提交、以及「选择并接诊」导入已有就诊人地址复用
*/
async function buildShipPayloadFromForm(): Promise<Record<string, string>> {
const f = form.value;
const detail = String(f.ship_detail_address || '').trim();
const hasRegion = !!(f.ship_region && f.ship_region.length >= 2);
if (!detail || !hasRegion) return {};
const resolved = await ensureResolvedAddress(f.ship_region);
const province = resolved.province?.label || shipLabels.value.province || '';
const city = resolved.city?.label || shipLabels.value.city || '';
const area = resolved.district?.label || shipLabels.value.area || '';
if (!province && !city && !area) {
throw new Error('省市区解析失败,请重新选择后再保存');
}
return {
ship_province: province,
ship_city: city,
ship_area: area,
ship_region:
resolved.pathLabel ||
formatAddressDisplay(f.ship_region) ||
[province, city, area].filter(Boolean).join(' '),
ship_detail_address: detail,
ship_name: String(f.ship_name || f.name || '').trim(),
ship_mobile: String(f.ship_mobile || f.mobile || '').trim(),
};
}
/** 本次收货预览文案(选择并接诊确认框用) */
function formatDraftShipPreviewText() {
const f = form.value;
const region =
formatAddressDisplay(f.ship_region) ||
[shipLabels.value.province, shipLabels.value.city, shipLabels.value.area]
.filter(Boolean)
.join('');
return formatShipAddressCopyText({
name: f.ship_name || f.name,
mobile: f.ship_mobile || f.mobile,
region,
detail_address: f.ship_detail_address,
});
}
/**
* 组装提交体:不展示的月经/婚育强制清零;history 仅在 status=有 时带出
* 收货地址:有详细地址才带省市区字段,后端据此建 yii_address
@@ -340,22 +403,7 @@ async function buildSubmitPayload() {
const f = form.value;
const menstrualOk = showMenstrualHistory.value;
const maritalOk = showMaritalHistory.value;
// 提交前按 ID 解析名称(含软删回显),避免本地树删除后取不到省市区文案
const resolved = await ensureResolvedAddress(f.ship_region);
const detail = String(f.ship_detail_address || '').trim();
const shipPayload =
detail && (resolved.province || resolved.city || resolved.district)
? {
ship_province: resolved.province?.label || '',
ship_city: resolved.city?.label || '',
ship_area: resolved.district?.label || '',
ship_region:
resolved.pathLabel || formatAddressDisplay(f.ship_region),
ship_detail_address: detail,
ship_name: String(f.ship_name || f.name || '').trim(),
ship_mobile: String(f.ship_mobile || f.mobile || '').trim(),
}
: {};
const shipPayload = await buildShipPayloadFromForm();
const {
ship_region: _shipRegion,
ship_detail_address: _shipDetail,
@@ -405,26 +453,97 @@ async function checkDuplicateBeforeCreate(): Promise<boolean> {
}
}
/** 选择已有就诊人并接诊(exact / 部分重复「使用已有」) */
async function selectExistingPatient(item: { id: number }) {
if (!item?.id) return;
/**
* 真正开始接诊(可选:先把本次收货地址导入到已有就诊人)
*/
async function doStartWithExisting(
item: { id: number; mobile?: string },
importShip: boolean,
) {
dupStartingId.value = item.id;
try {
if (importShip) {
const shipPayload = await buildShipPayloadFromForm();
if (Object.keys(shipPayload).length > 0) {
await updateCreatedUserPatientApi({
user_patient_id: item.id,
mobile: String(form.value.mobile || item.mobile || '').trim(),
...shipPayload,
});
}
}
const res = await startOfflineReceptionByPatientApi({
user_patient_id: item.id,
waive_register_fee: form.value.waive_register_fee ? 1 : 0,
});
message.success('已选择已有就诊人');
message.success(importShip ? '已导入地址并接诊' : '已选择已有就诊人');
importConfirmVisible.value = false;
dupVisible.value = false;
pendingDupItem.value = null;
modalApi.close();
await onSelectExistingCb.value?.(res as DoctorCreatedPatientStartResult);
} catch (error: any) {
message.error(error?.message || '接诊失败');
} finally {
dupStartingId.value = 0;
importConfirmLoading.value = false;
}
}
/**
* 选择已有就诊人并接诊;本次填了收货地址时先预览确认是否导入
*/
async function selectExistingPatient(item: {
id: number;
mobile?: string;
name?: string;
}) {
if (!item?.id) return;
// 未填本次地址:直接接诊
if (!hasFilledShipAddress()) {
await doStartWithExisting(item, false);
return;
}
dupStartingId.value = item.id;
try {
const detail = await getCreatedUserPatientDetailApi(item.id);
const oldShip = (detail?.ship_address || {}) as Record<string, string>;
const oldText = formatShipAddressCopyText(oldShip);
importHasOld.value = !!oldText;
importOldText.value = oldText;
importNewText.value = formatDraftShipPreviewText();
pendingDupItem.value = item;
importConfirmVisible.value = true;
} catch (error: any) {
message.error(error?.message || '加载就诊人地址失败');
} finally {
dupStartingId.value = 0;
}
}
/** 确认框:导入地址并接诊 */
async function confirmImportAndStart() {
const item = pendingDupItem.value;
if (!item?.id) return;
importConfirmLoading.value = true;
await doStartWithExisting(item, true);
}
/** 确认框:不导入,直接接诊 */
async function skipImportAndStart() {
const item = pendingDupItem.value;
if (!item?.id) return;
importConfirmLoading.value = true;
await doStartWithExisting(item, false);
}
/** 关闭导入确认,回到去重列表(不接诊) */
function closeImportConfirm() {
if (importConfirmLoading.value) return;
importConfirmVisible.value = false;
pendingDupItem.value = null;
}
/** 部分重复时仍创建新的 */
async function forceCreateNew() {
dupVisible.value = false;
@@ -507,6 +626,11 @@ async function applyClinicAddress() {
}
form.value.ship_region = [a.province_id, a.city_id];
form.value.ship_detail_address = a.position;
shipLabels.value = {
province: a.province || '',
city: a.city || '',
area: '',
};
await ensureResolvedAddress(form.value.ship_region);
if (a.contact) {
form.value.ship_name = a.contact.slice(0, 10);
@@ -589,6 +713,7 @@ const [Modal, modalApi] = useVbenModal({
onOpenChange(isOpen: boolean) {
if (isOpen) {
form.value = buildEmptyForm();
shipLabels.value = { province: '', city: '', area: '' };
clinicAddress.value = null;
forceCreate.value = 0;
dupVisible.value = false;
@@ -637,6 +762,12 @@ async function onAddressRecognized(result: AddressRecognizeResult) {
flash.detail = true;
}
if (result.province || result.city || result.area) {
// 先缓存中文省市区,即使后续 ID 解析失败也能提交
shipLabels.value = {
province: String(result.province || '').trim(),
city: String(result.city || '').trim(),
area: String(result.area || '').trim(),
};
flash.region = true;
await resolveShipRegionIds(result.province, result.city, result.area);
}
@@ -965,6 +1096,58 @@ function triggerFieldFlash(flash: {
</Button>
</div>
</AntModal>
<!-- 选择并接诊:本次有收货地址时确认是否导入(自定义 footer,避免点 X 误接诊) -->
<AntModal
v-model:open="importConfirmVisible"
title="导入本次收货地址?"
:mask-closable="false"
:closable="!importConfirmLoading"
centered
width="520px"
destroy-on-close
:footer="null"
@cancel="closeImportConfirm"
>
<p class="mb-3 text-sm text-[hsl(var(--muted-foreground))]">
本次填写了收货地址,确认后写入所选就诊人;也可跳过仅接诊。
</p>
<div
v-if="importHasOld"
class="mb-3 rounded border border-[hsl(var(--border))] bg-[hsl(var(--muted)/0.25)] px-3 py-2"
>
<div class="mb-1 text-xs text-[hsl(var(--muted-foreground))]">
原地址
</div>
<div class="break-all text-sm text-[hsl(var(--foreground))]">
{{ importOldText }}
</div>
</div>
<div
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))]">
{{ importHasOld ? '将覆盖为' : '将写入' }}
</div>
<div class="break-all text-sm text-[hsl(var(--foreground))]">
{{ importNewText || '—' }}
</div>
</div>
<div class="mt-4 flex justify-end gap-2">
<Button :disabled="importConfirmLoading" @click="closeImportConfirm">
返回
</Button>
<Button :loading="importConfirmLoading" @click="skipImportAndStart">
不导入,直接接诊
</Button>
<Button
type="primary"
:loading="importConfirmLoading"
@click="confirmImportAndStart"
>
导入并接诊
</Button>
</div>
</AntModal>
</Modal>
</template>

View File

@@ -57,6 +57,8 @@ const saving = ref(false);
const editMode = ref<'full' | 'limited'>('full');
const userPatientId = ref(0);
const clinicAddress = ref<ClinicAddress | null>(null);
/** 中文省市区兜底:详情回填/粘贴写入,ID 解析失败时仍能提交 */
const shipLabels = ref({ province: '', city: '', area: '' });
const form = ref({
name: '',
mobile: '',
@@ -115,6 +117,11 @@ async function applyClinicAddress() {
}
form.value.ship_region = [a.province_id, a.city_id];
form.value.ship_detail_address = a.position;
shipLabels.value = {
province: a.province || '',
city: a.city || '',
area: '',
};
await ensureResolvedAddress(form.value.ship_region);
if (a.contact) {
form.value.ship_name = a.contact.slice(0, 10);
@@ -152,6 +159,11 @@ async function loadDetail(id: number) {
form.value.ship_name = ship.name || '';
form.value.ship_mobile = ship.mobile || '';
form.value.ship_detail_address = ship.detail_address || '';
shipLabels.value = {
province: String(ship.province || '').trim(),
city: String(ship.city || '').trim(),
area: String(ship.area || '').trim(),
};
if (ship.province || ship.city || ship.area) {
const pair = await resolveAddressPairFromNames(
ship.province,
@@ -173,6 +185,11 @@ async function onAddressRecognized(result: AddressRecognizeResult) {
if (result.phone) form.value.ship_mobile = result.phone;
if (result.detail) form.value.ship_detail_address = result.detail;
if (result.province || result.city || result.area) {
shipLabels.value = {
province: String(result.province || '').trim(),
city: String(result.city || '').trim(),
area: String(result.area || '').trim(),
};
const pair = await resolveAddressPairFromNames(
result.province,
result.city,
@@ -185,21 +202,50 @@ async function onAddressRecognized(result: AddressRecognizeResult) {
}
}
/**
* 组装更新体:省市区优先 ID 解析,失败回落详情/粘贴缓存的中文名
* 有详细地址却无任何省市区文案时阻断,避免只写 detail、后端报错或半截更新
*/
async function buildPayload() {
const detail = String(form.value.ship_detail_address || '').trim();
const payload: Record<string, any> = {
user_patient_id: userPatientId.value,
mobile: form.value.mobile,
ship_name: form.value.ship_name,
ship_mobile: form.value.ship_mobile,
ship_detail_address: form.value.ship_detail_address,
ship_detail_address: detail,
};
if (form.value.ship_region?.length) {
const resolved = await ensureResolvedAddress(form.value.ship_region);
payload.ship_province = resolved.province?.label || '';
payload.ship_city = resolved.city?.label || '';
payload.ship_area = resolved.district?.label || '';
const province =
resolved.province?.label || shipLabels.value.province || '';
const city = resolved.city?.label || shipLabels.value.city || '';
const area = resolved.district?.label || shipLabels.value.area || '';
if (detail && !province && !city && !area) {
throw new Error('省市区解析失败,请重新选择后再保存');
}
payload.ship_province = province;
payload.ship_city = city;
payload.ship_area = area;
payload.ship_region =
resolved.pathLabel || formatAddressDisplay(form.value.ship_region);
resolved.pathLabel ||
formatAddressDisplay(form.value.ship_region) ||
[province, city, area].filter(Boolean).join(' ');
} else if (
detail &&
(shipLabels.value.province || shipLabels.value.city || shipLabels.value.area)
) {
// 有详情中文省市区但 ID 未解析出:仍提交中文,避免地址丢
payload.ship_province = shipLabels.value.province;
payload.ship_city = shipLabels.value.city;
payload.ship_area = shipLabels.value.area;
payload.ship_region = [
shipLabels.value.province,
shipLabels.value.city,
shipLabels.value.area,
]
.filter(Boolean)
.join(' ');
}
if (editMode.value === 'full') {
payload.name = form.value.name;