fix: 修复就诊人地址落库,并支持订单未同步ERP时改收货
选择并接诊时可预览确认导入本次地址;粘贴识别改为长到短取名,避免「罗朝亮」被截成「朝亮」。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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 }"
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user