feat: 接口参数复制带注释 JSON,下单自寄到仓展示仓库地址
All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m56s
All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m56s
便于调试时一键复制 Body/Response 参数树,并让客户在自寄到仓时快速查看与复制仓库收货信息。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a5ef7e542d
commit
2b3797d548
@ -42,6 +42,8 @@
|
||||
} from '@/utils/interfaceParameterModel';
|
||||
import {
|
||||
applyJsonCommentsToExistingRows,
|
||||
buildCommentedJsonFromKvRows,
|
||||
buildCommentedJsonFromParameterFlatList,
|
||||
defaultResolveParameterType,
|
||||
extractCommentsFromJson,
|
||||
flatParameterListToTableRows,
|
||||
@ -2691,16 +2693,11 @@
|
||||
dataList1.value = undefined;
|
||||
}
|
||||
|
||||
// 复制 cURL tab 输入框内容到剪贴板
|
||||
async function copyRequestExampleContent() {
|
||||
const content = requestExample.value || '';
|
||||
if (!content.trim()) {
|
||||
Message.warning('暂无内容可复制');
|
||||
return;
|
||||
}
|
||||
async function writeTextToClipboard(content: string) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content);
|
||||
Message.success('已复制到剪贴板');
|
||||
return true;
|
||||
} catch {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = content;
|
||||
@ -2711,12 +2708,66 @@
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
Message.success('已复制到剪贴板');
|
||||
return true;
|
||||
} catch {
|
||||
Message.error('复制失败');
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 复制 cURL tab 输入框内容到剪贴板
|
||||
async function copyRequestExampleContent() {
|
||||
const content = requestExample.value || '';
|
||||
if (!content.trim()) {
|
||||
Message.warning('暂无内容可复制');
|
||||
return;
|
||||
}
|
||||
await writeTextToClipboard(content);
|
||||
}
|
||||
|
||||
/** 复制当前 Body 参数列表为带 // 描述的格式化 JSON */
|
||||
async function copyBodyParamsAsCommentedJson() {
|
||||
if (isNoneBodyContentType.value) {
|
||||
Message.warning('当前请求类型为 none,无 Body 参数可复制');
|
||||
return;
|
||||
}
|
||||
|
||||
let content = '';
|
||||
if (isBodyKvMode.value) {
|
||||
const rows = getTableFullData(bodyFormTableRef, bodyFormTableProps.data);
|
||||
content = buildCommentedJsonFromKvRows(rows);
|
||||
} else {
|
||||
const rows = getRequestBodyFlatParams();
|
||||
content = buildCommentedJsonFromParameterFlatList(rows, {
|
||||
isRootRow: isBodyRootRow,
|
||||
});
|
||||
}
|
||||
|
||||
const compact = content.replace(/\s+/g, '');
|
||||
if (!compact || compact === '{}' || compact === '[]') {
|
||||
Message.warning('暂无参数可复制');
|
||||
return;
|
||||
}
|
||||
await writeTextToClipboard(content);
|
||||
}
|
||||
|
||||
/** 复制当前 Response Body 参数列表为带 // 描述的格式化 JSON */
|
||||
async function copyResponseParamsAsCommentedJson() {
|
||||
const fromGrid =
|
||||
responseTableRef.value?.getTableData?.()?.fullData
|
||||
|| responseTableRef.value?.xGrid?.getTableData()?.fullData;
|
||||
const rows = getTreeTableFlatRows(fromGrid, responseTableProps.data);
|
||||
const content = buildCommentedJsonFromParameterFlatList(rows);
|
||||
const compact = content.replace(/\s+/g, '');
|
||||
if (!compact || compact === '{}' || compact === '[]') {
|
||||
Message.warning('暂无参数可复制');
|
||||
return;
|
||||
}
|
||||
await writeTextToClipboard(content);
|
||||
}
|
||||
|
||||
// 取消 Request 参数解析
|
||||
function cancelClick() {
|
||||
@ -3656,6 +3707,14 @@
|
||||
aria-label="Request content type"
|
||||
@change="updateContentType"
|
||||
/>
|
||||
<a-button
|
||||
size="mini"
|
||||
class="content-type-copy-btn"
|
||||
title="复制当前参数为带描述的 JSON"
|
||||
@click="copyBodyParamsAsCommentedJson"
|
||||
>
|
||||
复制
|
||||
</a-button>
|
||||
</div>
|
||||
<!-- Query tab 内容 -->
|
||||
<div v-show="requestTab === 'query'" class="query-tab-pane">
|
||||
@ -4428,6 +4487,14 @@
|
||||
aria-label="Response content type"
|
||||
@change="updateResponseContentType"
|
||||
/>
|
||||
<a-button
|
||||
size="mini"
|
||||
class="content-type-copy-btn"
|
||||
title="复制当前参数为带描述的 JSON"
|
||||
@click="copyResponseParamsAsCommentedJson"
|
||||
>
|
||||
复制
|
||||
</a-button>
|
||||
</div>
|
||||
<!-- Response body tab 内容 -->
|
||||
<div v-show="responseTab === 'body'" class="table-content">
|
||||
@ -5149,7 +5216,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--app-button-gap, 8px);
|
||||
padding: 0 0 10px var(--vxe-table-cell-padding-left, 10px);
|
||||
padding: 0 var(--vxe-table-cell-padding-left, 10px) 10px;
|
||||
min-height: 28px;
|
||||
box-sizing: border-box;
|
||||
background: transparent;
|
||||
@ -5159,6 +5226,14 @@
|
||||
.content-type-tabs {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.content-type-copy-btn {
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
height: 24px;
|
||||
padding: 0 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Apifox 式虚拟根节点(仅 Json/Xml 展示,导出剥离) */
|
||||
|
||||
@ -2425,6 +2425,19 @@ export default {
|
||||
'For multiple shipments, select waybills in Waybill Manage to create pickups in bulk',
|
||||
'order.placeOrder.single.doorPickupBatchHintCustomer':
|
||||
'For multiple shipments, select orders in My Orders to create pickups in bulk',
|
||||
'order.placeOrder.single.selfShipAddressTitle': 'Ship to warehouse',
|
||||
'order.placeOrder.single.selfShipRecipient': 'Recipient',
|
||||
'order.placeOrder.single.selfShipPhone': 'Phone',
|
||||
'order.placeOrder.single.selfShipAddress': 'Address',
|
||||
'order.placeOrder.single.selfShipClientNo': 'Client No.',
|
||||
'order.placeOrder.single.selfShipCopy': 'Copy',
|
||||
'order.placeOrder.single.selfShipCopySuccess': 'Warehouse shipping info copied',
|
||||
'order.placeOrder.single.selfShipCopyFailed': 'Copy failed',
|
||||
'order.placeOrder.single.selfShipSelectWarehouse': 'Select a warehouse above first',
|
||||
'order.placeOrder.single.selfShipAddressEmpty':
|
||||
'This warehouse has no shipping address yet. Contact support or choose another warehouse.',
|
||||
'order.placeOrder.single.selfShipAddressHint':
|
||||
'Ship to this address yourself. Put the client number on the courier label so the warehouse can match it.',
|
||||
'order.placeOrder.single.submitHint': 'Complete required fields and accept the agreement to submit',
|
||||
'order.placeOrder.single.submitReady': 'Ready to submit',
|
||||
'order.placeOrder.contact.blockTitle': 'Complete your contact info',
|
||||
|
||||
@ -2318,6 +2318,18 @@ export default {
|
||||
'多票上门揽收可在「运单管理」中勾选运单后批量创建揽收单',
|
||||
'order.placeOrder.single.doorPickupBatchHintCustomer':
|
||||
'多票上门揽收可在「我的订单」中勾选运单后批量创建揽收单',
|
||||
'order.placeOrder.single.selfShipAddressTitle': '寄往仓库',
|
||||
'order.placeOrder.single.selfShipRecipient': '收件人',
|
||||
'order.placeOrder.single.selfShipPhone': '电话',
|
||||
'order.placeOrder.single.selfShipAddress': '地址',
|
||||
'order.placeOrder.single.selfShipClientNo': '客户单号',
|
||||
'order.placeOrder.single.selfShipCopy': '复制',
|
||||
'order.placeOrder.single.selfShipCopySuccess': '仓库寄件信息已复制',
|
||||
'order.placeOrder.single.selfShipCopyFailed': '复制失败',
|
||||
'order.placeOrder.single.selfShipSelectWarehouse': '请先选择上方仓库',
|
||||
'order.placeOrder.single.selfShipAddressEmpty': '该仓库暂未配置收货地址,请联系客服或更换仓库',
|
||||
'order.placeOrder.single.selfShipAddressHint':
|
||||
'按此地址自行寄出。快递面单建议备注客户单号,方便仓库认领。',
|
||||
'order.placeOrder.single.submitHint': '请完善必填信息并勾选协议后提交',
|
||||
'order.placeOrder.single.submitReady': '信息已完善,可提交下单',
|
||||
'order.placeOrder.contact.blockTitle': '请先完善联系方式',
|
||||
|
||||
@ -586,3 +586,233 @@ export function reorderJsonBodiesForMapping(
|
||||
if (!best) return candidates;
|
||||
return [best, ...candidates.filter((c) => c !== best)];
|
||||
}
|
||||
|
||||
function escapeJsonComment(text: string): string {
|
||||
return String(text || '')
|
||||
.replace(/\r?\n/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function sampleLiteralByParameterType(type: unknown): string {
|
||||
const t = Number(type);
|
||||
if (t === ParameterType.Integer || t === ParameterType.Float) return '0';
|
||||
if (t === ParameterType.Boolean) return 'false';
|
||||
if (t === ParameterType.Object) return '{}';
|
||||
if (t === ParameterType.Array) return '[]';
|
||||
return '""';
|
||||
}
|
||||
|
||||
function isTopLevelParameterPid(pId: unknown): boolean {
|
||||
return pId == null || pId === '' || pId === 0 || pId === '0';
|
||||
}
|
||||
|
||||
export interface BuildCommentedJsonOptions {
|
||||
isRootRow?: (row: any) => boolean;
|
||||
indentSize?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将参数树扁平列表导出为带 // 参数描述的 JSON(JSON5 风格,可再导入注释)。
|
||||
* 叶子用类型占位值;Object/Array 按子节点展开;跳过虚拟根与 obj/val/array 占位名。
|
||||
*/
|
||||
export function buildCommentedJsonFromParameterFlatList(
|
||||
rows: any[],
|
||||
options?: BuildCommentedJsonOptions,
|
||||
): string {
|
||||
const list = (Array.isArray(rows) ? rows : []).filter(Boolean);
|
||||
const isRootRow = options?.isRootRow ?? (() => false);
|
||||
const indentSize = options?.indentSize ?? 2;
|
||||
const pad = (depth: number) => ' '.repeat(Math.max(0, depth) * indentSize);
|
||||
|
||||
const byId = new Map<any, any>();
|
||||
list.forEach((r) => {
|
||||
if (r?.id != null) byId.set(r.id, r);
|
||||
});
|
||||
|
||||
const childrenOf = new Map<any, any[]>();
|
||||
const ensureBucket = (key: any) => {
|
||||
if (!childrenOf.has(key)) childrenOf.set(key, []);
|
||||
return childrenOf.get(key)!;
|
||||
};
|
||||
|
||||
list.forEach((row) => {
|
||||
if (isRootRow(row)) return;
|
||||
const pId = row.pId ?? row.PId;
|
||||
const parent = byId.get(pId);
|
||||
const bucketKey =
|
||||
isTopLevelParameterPid(pId) || (parent && isRootRow(parent)) ? 0 : pId;
|
||||
ensureBucket(bucketKey).push(row);
|
||||
});
|
||||
|
||||
const getChildren = (id: any) => childrenOf.get(id) || [];
|
||||
|
||||
function formatComment(desc: unknown): string {
|
||||
const text = escapeJsonComment(String(desc ?? ''));
|
||||
return text ? ` // ${text}` : '';
|
||||
}
|
||||
|
||||
function renderObjectProps(nodes: any[], depth: number): string {
|
||||
const props = nodes.filter((n) => {
|
||||
const name = String(n?.name ?? '').trim();
|
||||
return name && !SYNTHETIC_PARAM_NAMES.has(name);
|
||||
});
|
||||
if (!props.length) return `${pad(depth)}{}`;
|
||||
|
||||
const lines: string[] = [`${pad(depth)}{`];
|
||||
props.forEach((node, index) => {
|
||||
const comma = index < props.length - 1 ? ',' : '';
|
||||
lines.push(...renderPropertyLines(node, depth + 1, comma));
|
||||
});
|
||||
lines.push(`${pad(depth)}}`);
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function renderPropertyLines(node: any, depth: number, comma: string): string[] {
|
||||
const name = String(node?.name ?? '').trim();
|
||||
const key = JSON.stringify(name);
|
||||
const comment = formatComment(node?.description);
|
||||
const type = Number(node?.type);
|
||||
const kids = getChildren(node.id);
|
||||
const indent = pad(depth);
|
||||
|
||||
if (type === ParameterType.Object || (kids.length > 0 && type !== ParameterType.Array)) {
|
||||
const innerProps = kids.filter((k) => {
|
||||
const n = String(k?.name ?? '').trim();
|
||||
return n && !SYNTHETIC_PARAM_NAMES.has(n);
|
||||
});
|
||||
if (!innerProps.length && type === ParameterType.Object) {
|
||||
return [`${indent}${key}: {}${comma}${comment}`];
|
||||
}
|
||||
const lines: string[] = [`${indent}${key}: {${comment}`];
|
||||
innerProps.forEach((child, index) => {
|
||||
const childComma = index < innerProps.length - 1 ? ',' : '';
|
||||
lines.push(...renderPropertyLines(child, depth + 1, childComma));
|
||||
});
|
||||
lines.push(`${indent}}${comma}`);
|
||||
return lines;
|
||||
}
|
||||
|
||||
if (type === ParameterType.Array) {
|
||||
return renderArrayPropertyLines(node, depth, comma, key, comment, kids);
|
||||
}
|
||||
|
||||
return [`${indent}${key}: ${sampleLiteralByParameterType(type)}${comma}${comment}`];
|
||||
}
|
||||
|
||||
function renderArrayPropertyLines(
|
||||
_node: any,
|
||||
depth: number,
|
||||
comma: string,
|
||||
key: string,
|
||||
comment: string,
|
||||
kids: any[],
|
||||
): string[] {
|
||||
const indent = pad(depth);
|
||||
const objChild = kids.find((k) => {
|
||||
const n = String(k?.name ?? '').trim().toLowerCase();
|
||||
return n === 'obj' || Number(k?.type) === ParameterType.Object;
|
||||
});
|
||||
const valChild = kids.find((k) => {
|
||||
const n = String(k?.name ?? '').trim().toLowerCase();
|
||||
return n === 'val';
|
||||
});
|
||||
|
||||
if (objChild) {
|
||||
const fields = getChildren(objChild.id).filter((k) => {
|
||||
const n = String(k?.name ?? '').trim();
|
||||
return n && !SYNTHETIC_PARAM_NAMES.has(n);
|
||||
});
|
||||
if (!fields.length) {
|
||||
return [`${indent}${key}: [{}]${comma}${comment}`];
|
||||
}
|
||||
const lines: string[] = [`${indent}${key}: [${comment}`, `${pad(depth + 1)}{`];
|
||||
fields.forEach((child, index) => {
|
||||
const childComma = index < fields.length - 1 ? ',' : '';
|
||||
lines.push(...renderPropertyLines(child, depth + 2, childComma));
|
||||
});
|
||||
lines.push(`${pad(depth + 1)}}`, `${indent}]${comma}`);
|
||||
return lines;
|
||||
}
|
||||
|
||||
if (valChild) {
|
||||
const sample = sampleLiteralByParameterType(valChild.type);
|
||||
return [`${indent}${key}: [${sample}]${comma}${comment}`];
|
||||
}
|
||||
|
||||
const namedKids = kids.filter((k) => {
|
||||
const n = String(k?.name ?? '').trim();
|
||||
return n && !SYNTHETIC_PARAM_NAMES.has(n);
|
||||
});
|
||||
if (namedKids.length) {
|
||||
const lines: string[] = [`${indent}${key}: [${comment}`, `${pad(depth + 1)}{`];
|
||||
namedKids.forEach((child, index) => {
|
||||
const childComma = index < namedKids.length - 1 ? ',' : '';
|
||||
lines.push(...renderPropertyLines(child, depth + 2, childComma));
|
||||
});
|
||||
lines.push(`${pad(depth + 1)}}`, `${indent}]${comma}`);
|
||||
return lines;
|
||||
}
|
||||
|
||||
return [`${indent}${key}: []${comma}${comment}`];
|
||||
}
|
||||
|
||||
const tops = getChildren(0);
|
||||
if (!tops.length) return '{\n}\n';
|
||||
|
||||
if (tops.length === 1 && isSyntheticArrayWrapper(tops[0])) {
|
||||
const wrapper = tops[0];
|
||||
const kids = getChildren(wrapper.id);
|
||||
const objChild = kids.find((k) => String(k?.name ?? '').trim().toLowerCase() === 'obj');
|
||||
const valChild = kids.find((k) => String(k?.name ?? '').trim().toLowerCase() === 'val');
|
||||
|
||||
if (objChild) {
|
||||
const fields = getChildren(objChild.id).filter((k) => {
|
||||
const n = String(k?.name ?? '').trim();
|
||||
return n && !SYNTHETIC_PARAM_NAMES.has(n);
|
||||
});
|
||||
if (!fields.length) return '[{}]\n';
|
||||
return `[\n${renderObjectProps(fields, 1)}\n]\n`;
|
||||
}
|
||||
|
||||
if (valChild) {
|
||||
return `[${sampleLiteralByParameterType(valChild.type)}]\n`;
|
||||
}
|
||||
|
||||
const namedKids = kids.filter((k) => {
|
||||
const n = String(k?.name ?? '').trim();
|
||||
return n && !SYNTHETIC_PARAM_NAMES.has(n);
|
||||
});
|
||||
if (namedKids.length) {
|
||||
return `[\n${renderObjectProps(namedKids, 1)}\n]\n`;
|
||||
}
|
||||
return '[]\n';
|
||||
}
|
||||
|
||||
return `${renderObjectProps(tops, 0)}\n`;
|
||||
}
|
||||
|
||||
/** FormUrlEncoded / 扁平 KV 列表 → 带 // 描述的 JSON 对象 */
|
||||
export function buildCommentedJsonFromKvRows(rows: any[]): string {
|
||||
const items = (Array.isArray(rows) ? rows : []).filter((r) => {
|
||||
const name = String(r?.name ?? '').trim();
|
||||
return !!name;
|
||||
});
|
||||
if (!items.length) return '{\n}\n';
|
||||
|
||||
const lines: string[] = ['{'];
|
||||
items.forEach((row, index) => {
|
||||
const name = String(row.name).trim();
|
||||
const rawValue = row.value;
|
||||
const value =
|
||||
rawValue == null || rawValue === ''
|
||||
? '""'
|
||||
: JSON.stringify(String(rawValue));
|
||||
const desc = escapeJsonComment(String(row.description ?? ''));
|
||||
const comma = index < items.length - 1 ? ',' : '';
|
||||
const comment = desc ? ` // ${desc}` : '';
|
||||
lines.push(` ${JSON.stringify(name)}: ${value}${comma}${comment}`);
|
||||
});
|
||||
lines.push('}');
|
||||
return `${lines.join('\n')}\n`;
|
||||
}
|
||||
|
||||
@ -231,6 +231,45 @@
|
||||
<p v-if="form.inboundMethod === 'doorPickup'" class="h5-po__hint">
|
||||
{{ t('h5.placeOrder.inboundDoorPickupHint') }}
|
||||
</p>
|
||||
<div v-if="form.inboundMethod === 'selfShip'" class="h5-po__wh-card">
|
||||
<div class="h5-po__wh-head">
|
||||
<strong>
|
||||
{{ t('h5.placeOrder.selfShipAddressTitle') }}
|
||||
<template v-if="selectedWarehouseOption?.label">
|
||||
· {{ selectedWarehouseOption.label }}
|
||||
</template>
|
||||
</strong>
|
||||
<button
|
||||
v-if="selectedWarehouseHasAddress"
|
||||
type="button"
|
||||
class="h5-po__wh-copy"
|
||||
@click="copyWarehouseShipAddress"
|
||||
>
|
||||
{{ t('h5.placeOrder.selfShipCopy') }}
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="!selectedWarehouseOption" class="h5-po__hint">
|
||||
{{ t('h5.placeOrder.selfShipSelectWarehouse') }}
|
||||
</p>
|
||||
<template v-else-if="!selectedWarehouseHasAddress">
|
||||
<p class="h5-po__hint">{{ t('h5.placeOrder.selfShipAddressEmpty') }}</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>
|
||||
<span>{{ t('h5.placeOrder.selfShipRecipient') }}</span>
|
||||
{{ selectedWarehouseOption.recipient || '-' }}
|
||||
</p>
|
||||
<p>
|
||||
<span>{{ t('h5.placeOrder.selfShipPhone') }}</span>
|
||||
{{ selectedWarehouseOption.contactPhone || '-' }}
|
||||
</p>
|
||||
<p class="is-addr">
|
||||
<span>{{ t('h5.placeOrder.selfShipAddress') }}</span>
|
||||
{{ selectedWarehouseOption.shippingAddress || '-' }}
|
||||
</p>
|
||||
<p class="h5-po__hint">{{ t('h5.placeOrder.selfShipAddressHint') }}</p>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="form.inboundMethod === 'doorPickup'">
|
||||
@ -1062,7 +1101,15 @@ const steps = computed(() => [
|
||||
|
||||
const productOptions = ref<{ value: string; label: string; productId?: number }[]>([]);
|
||||
const cargoOptions = ref<{ value: string; label: string }[]>([]);
|
||||
const warehouseOptions = ref<{ value: string; label: string }[]>([]);
|
||||
const warehouseOptions = ref<
|
||||
{
|
||||
value: string;
|
||||
label: string;
|
||||
recipient?: string;
|
||||
contactPhone?: string;
|
||||
shippingAddress?: string;
|
||||
}[]
|
||||
>([]);
|
||||
const countryOptions = ref<{ value: string; label: string }[]>([]);
|
||||
const currencyOptions = ref<{ value: string; label: string }[]>([]);
|
||||
const senderOptions = ref<{ id: number; label: string; data: Record<string, string> }[]>([]);
|
||||
@ -1242,6 +1289,36 @@ const form = reactive({
|
||||
},
|
||||
});
|
||||
|
||||
const selectedWarehouseOption = computed(() => {
|
||||
const id = String(form.warehouseId ?? '').trim();
|
||||
if (!id) return undefined;
|
||||
return warehouseOptions.value.find((opt) => String(opt.value) === id);
|
||||
});
|
||||
const selectedWarehouseHasAddress = computed(() => {
|
||||
const opt = selectedWarehouseOption.value;
|
||||
if (!opt) return false;
|
||||
return Boolean(opt.recipient || opt.contactPhone || opt.shippingAddress);
|
||||
});
|
||||
|
||||
async function copyWarehouseShipAddress() {
|
||||
const opt = selectedWarehouseOption.value;
|
||||
if (!opt) return;
|
||||
const text = [
|
||||
opt.label || '',
|
||||
`${t('h5.placeOrder.selfShipRecipient')}:${opt.recipient || '-'}`,
|
||||
`${t('h5.placeOrder.selfShipPhone')}:${opt.contactPhone || '-'}`,
|
||||
`${t('h5.placeOrder.selfShipAddress')}:${opt.shippingAddress || '-'}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
Message.success(t('h5.placeOrder.selfShipCopySuccess'));
|
||||
} catch {
|
||||
Message.error(t('h5.orders.copyFailed'));
|
||||
}
|
||||
}
|
||||
|
||||
function trim(v: unknown) {
|
||||
return String(v ?? '').trim();
|
||||
}
|
||||
@ -2556,7 +2633,20 @@ async function loadOptions(
|
||||
.map((row: any) => {
|
||||
const id = row?.id ?? row?.Id;
|
||||
const name = String(row?.warehouseName ?? row?.WarehouseName ?? '').trim();
|
||||
return { value: String(id ?? ''), label: name || String(id) };
|
||||
const pick = (...keys: string[]) => {
|
||||
for (const k of keys) {
|
||||
const v = row?.[k];
|
||||
if (v !== undefined && v !== null && String(v).trim() !== '') return String(v).trim();
|
||||
}
|
||||
return '';
|
||||
};
|
||||
return {
|
||||
value: String(id ?? ''),
|
||||
label: name || String(id),
|
||||
recipient: pick('recipient', 'Recipient'),
|
||||
contactPhone: pick('contactPhone', 'ContactPhone'),
|
||||
shippingAddress: pick('shippingAddress', 'ShippingAddress'),
|
||||
};
|
||||
})
|
||||
.filter((x) => x.value);
|
||||
if (!opts?.skipDefaults && warehouseOptions.value.length && !form.warehouseId) {
|
||||
@ -3030,6 +3120,50 @@ watch(
|
||||
color: #86909c;
|
||||
}
|
||||
|
||||
.h5-po__wh-card {
|
||||
margin-top: 8px;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid color-mix(in srgb, var(--app-primary-color) 16%, #e5e6eb);
|
||||
}
|
||||
|
||||
.h5-po__wh-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
strong {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.h5-po__wh-copy {
|
||||
border: none;
|
||||
background: color-mix(in srgb, var(--app-primary-color) 12%, #fff);
|
||||
color: var(--app-primary-color);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.h5-po__wh-card p {
|
||||
margin: 8px 0 0;
|
||||
font-size: 13px;
|
||||
color: #1d2129;
|
||||
line-height: 1.45;
|
||||
span {
|
||||
color: #86909c;
|
||||
margin-right: 6px;
|
||||
}
|
||||
&.is-addr {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.h5-po__more-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@ -393,6 +393,17 @@ export default {
|
||||
'h5.placeOrder.inboundDoorPickup': 'Door pickup',
|
||||
'h5.placeOrder.inboundDoorPickupHint':
|
||||
'Door pickup is a paid service. Please fill in the pickup address and expected time.',
|
||||
'h5.placeOrder.selfShipAddressTitle': 'Ship to warehouse',
|
||||
'h5.placeOrder.selfShipRecipient': 'Recipient',
|
||||
'h5.placeOrder.selfShipPhone': 'Phone',
|
||||
'h5.placeOrder.selfShipAddress': 'Address',
|
||||
'h5.placeOrder.selfShipCopy': 'Copy',
|
||||
'h5.placeOrder.selfShipCopySuccess': 'Warehouse shipping info copied',
|
||||
'h5.placeOrder.selfShipSelectWarehouse': 'Select a warehouse first',
|
||||
'h5.placeOrder.selfShipAddressEmpty':
|
||||
'This warehouse has no shipping address yet. Contact support or choose another warehouse.',
|
||||
'h5.placeOrder.selfShipAddressHint':
|
||||
'Ship to this address yourself. Put the client number on the courier label so the warehouse can match it.',
|
||||
'h5.placeOrder.pickupAddress': 'Pickup address',
|
||||
'h5.placeOrder.pickupAddressPh': 'Select a saved sender address',
|
||||
'h5.placeOrder.expectPickupTime': 'Expected pickup time',
|
||||
|
||||
@ -391,6 +391,15 @@ export default {
|
||||
'h5.placeOrder.inboundSelfShip': '自寄到仓',
|
||||
'h5.placeOrder.inboundDoorPickup': '上门揽收',
|
||||
'h5.placeOrder.inboundDoorPickupHint': '上门揽收为付费服务,请填写取件地址与期望揽收时间。',
|
||||
'h5.placeOrder.selfShipAddressTitle': '寄往仓库',
|
||||
'h5.placeOrder.selfShipRecipient': '收件人',
|
||||
'h5.placeOrder.selfShipPhone': '电话',
|
||||
'h5.placeOrder.selfShipAddress': '地址',
|
||||
'h5.placeOrder.selfShipCopy': '复制',
|
||||
'h5.placeOrder.selfShipCopySuccess': '仓库寄件信息已复制',
|
||||
'h5.placeOrder.selfShipSelectWarehouse': '请先选择仓库',
|
||||
'h5.placeOrder.selfShipAddressEmpty': '该仓库暂未配置收货地址,请联系客服或更换仓库',
|
||||
'h5.placeOrder.selfShipAddressHint': '按此地址自行寄出。快递面单建议备注客户单号,方便仓库认领。',
|
||||
'h5.placeOrder.pickupAddress': '取件地址',
|
||||
'h5.placeOrder.pickupAddressPh': '请选择发件地址簿',
|
||||
'h5.placeOrder.expectPickupTime': '期望揽收时间',
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
IconEdit,
|
||||
IconQuestionCircle,
|
||||
IconCheck,
|
||||
IconCopy,
|
||||
} from '@arco-design/web-vue/es/icon';
|
||||
import SlidingTabs from '@/components/SlidingTabs/index.vue';
|
||||
import ExcelFileIcon from '@/components/icons/ExcelFileIcon.vue';
|
||||
@ -311,7 +312,12 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
const singleDomesticPickupProductOptions = ref<Array<{ value: string; label: string }>>([]);
|
||||
const singleCargoAttributeOptions = ref<SingleSelectOption[]>([]);
|
||||
const singleExportCustomsModeOptions = ref<SingleSelectOption[]>([]);
|
||||
const singleWarehouseOptions = ref<SingleSelectOption[]>([]);
|
||||
type WarehouseSelectOption = SingleSelectOption & {
|
||||
recipient?: string;
|
||||
contactPhone?: string;
|
||||
shippingAddress?: string;
|
||||
};
|
||||
const singleWarehouseOptions = ref<WarehouseSelectOption[]>([]);
|
||||
const singleCurrencyOptions = ref<SingleSelectOption[]>([]);
|
||||
const singleCountryOptions = ref<SingleSelectOption[]>([]);
|
||||
|
||||
@ -346,7 +352,20 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
function normalizeWarehouseRow(row: any) {
|
||||
const id = row?.id ?? row?.Id;
|
||||
const name = String(row?.warehouseName ?? row?.WarehouseName ?? '').trim();
|
||||
return { id: id != null && id !== '' ? String(id) : '', name };
|
||||
const pick = (...keys: string[]) => {
|
||||
for (const k of keys) {
|
||||
const v = row?.[k];
|
||||
if (v !== undefined && v !== null && String(v).trim() !== '') return String(v).trim();
|
||||
}
|
||||
return '';
|
||||
};
|
||||
return {
|
||||
id: id != null && id !== '' ? String(id) : '',
|
||||
name,
|
||||
recipient: pick('recipient', 'Recipient'),
|
||||
contactPhone: pick('contactPhone', 'ContactPhone'),
|
||||
shippingAddress: pick('shippingAddress', 'ShippingAddress'),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeCountryRow(row: any) {
|
||||
@ -1999,6 +2018,60 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
],
|
||||
});
|
||||
|
||||
const selectedWarehouseOption = computed(() => {
|
||||
const id = String(singleForm.warehouseId ?? '').trim();
|
||||
if (!id) return undefined;
|
||||
return singleWarehouseOptions.value.find((opt) => String(opt.value) === id);
|
||||
});
|
||||
|
||||
const selectedWarehouseHasAddress = computed(() => {
|
||||
const opt = selectedWarehouseOption.value;
|
||||
if (!opt) return false;
|
||||
return Boolean(opt.recipient || opt.contactPhone || opt.shippingAddress);
|
||||
});
|
||||
|
||||
function buildWarehouseShipCopyText() {
|
||||
const opt = selectedWarehouseOption.value;
|
||||
if (!opt) return '';
|
||||
const lines = [
|
||||
opt.label || '',
|
||||
`收件人:${opt.recipient || '-'}`,
|
||||
`电话:${opt.contactPhone || '-'}`,
|
||||
`地址:${opt.shippingAddress || '-'}`,
|
||||
];
|
||||
const clientNo = String(singleForm.clientNo ?? '').trim();
|
||||
if (clientNo) {
|
||||
lines.push(`客户单号:${clientNo}`);
|
||||
}
|
||||
return lines.filter(Boolean).join('\n');
|
||||
}
|
||||
|
||||
async function copyWarehouseShipAddress() {
|
||||
const text = buildWarehouseShipCopyText();
|
||||
if (!text.trim()) {
|
||||
Message.warning('该仓库暂未配置收货地址');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
Message.success('仓库寄件信息已复制');
|
||||
} catch {
|
||||
const area = document.createElement('textarea');
|
||||
area.value = text;
|
||||
area.style.position = 'fixed';
|
||||
area.style.left = '-9999px';
|
||||
document.body.appendChild(area);
|
||||
area.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
Message.success('仓库寄件信息已复制');
|
||||
} catch {
|
||||
Message.error('复制失败');
|
||||
}
|
||||
document.body.removeChild(area);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveDefaultDeclaredCurrency() {
|
||||
const opts = singleCurrencyOptions.value;
|
||||
const usd = opts.find((o) => String(o.value).trim().toUpperCase() === 'USD');
|
||||
@ -2247,7 +2320,13 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
const whOpts = (warehouseRes.items ?? [])
|
||||
.map(normalizeWarehouseRow)
|
||||
.filter((w) => w.id)
|
||||
.map((w) => ({ value: w.id, label: w.name || w.id }));
|
||||
.map((w) => ({
|
||||
value: w.id,
|
||||
label: w.name || w.id,
|
||||
recipient: w.recipient,
|
||||
contactPhone: w.contactPhone,
|
||||
shippingAddress: w.shippingAddress,
|
||||
}));
|
||||
singleWarehouseOptions.value = whOpts;
|
||||
if (whOpts.length && !String(singleForm.warehouseId ?? '').trim()) {
|
||||
singleForm.warehouseId = whOpts[0].value;
|
||||
@ -3616,7 +3695,71 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
size="small"
|
||||
class="inbound-method-radios"
|
||||
>
|
||||
<a-radio value="selfShip">自寄到仓</a-radio>
|
||||
<a-radio value="selfShip">
|
||||
<span class="inbound-method-pickup-label">
|
||||
自寄到仓
|
||||
<a-popover
|
||||
position="bl"
|
||||
trigger="hover"
|
||||
content-class="warehouse-ship-popover"
|
||||
>
|
||||
<IconQuestionCircle
|
||||
class="inbound-method-help"
|
||||
@click.stop
|
||||
/>
|
||||
<template #content>
|
||||
<div class="warehouse-ship-popover__body">
|
||||
<div class="warehouse-ship-popover__head">
|
||||
<strong>
|
||||
寄往仓库
|
||||
<template v-if="selectedWarehouseOption?.label">
|
||||
· {{ selectedWarehouseOption.label }}
|
||||
</template>
|
||||
</strong>
|
||||
<a-button
|
||||
v-if="selectedWarehouseHasAddress"
|
||||
type="text"
|
||||
size="mini"
|
||||
class="warehouse-ship-popover__copy"
|
||||
@click="copyWarehouseShipAddress"
|
||||
>
|
||||
<template #icon><IconCopy /></template>
|
||||
复制
|
||||
</a-button>
|
||||
</div>
|
||||
<p v-if="!selectedWarehouseOption" class="warehouse-ship-popover__empty">
|
||||
请先选择上方仓库
|
||||
</p>
|
||||
<p
|
||||
v-else-if="!selectedWarehouseHasAddress"
|
||||
class="warehouse-ship-popover__empty"
|
||||
>
|
||||
该仓库暂未配置收货地址,请联系客服或更换仓库
|
||||
</p>
|
||||
<template v-else>
|
||||
<dl class="warehouse-ship-popover__rows">
|
||||
<div>
|
||||
<dt>收件人</dt>
|
||||
<dd>{{ selectedWarehouseOption.recipient || '-' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>电话</dt>
|
||||
<dd>{{ selectedWarehouseOption.contactPhone || '-' }}</dd>
|
||||
</div>
|
||||
<div class="is-addr">
|
||||
<dt>地址</dt>
|
||||
<dd>{{ selectedWarehouseOption.shippingAddress || '-' }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p class="warehouse-ship-popover__hint">
|
||||
按此地址自行寄出。快递面单建议备注客户单号,方便仓库认领。
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
</span>
|
||||
</a-radio>
|
||||
<a-radio value="doorPickup">
|
||||
<span class="inbound-method-pickup-label">
|
||||
上门揽收
|
||||
@ -6836,3 +6979,77 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.warehouse-ship-popover {
|
||||
max-width: 360px;
|
||||
padding: 0;
|
||||
|
||||
.arco-popover-content {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.warehouse-ship-popover__body {
|
||||
min-width: 240px;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.warehouse-ship-popover__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
|
||||
strong {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.warehouse-ship-popover__copy {
|
||||
height: 24px;
|
||||
padding: 0 8px;
|
||||
color: var(--app-primary-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.warehouse-ship-popover__rows {
|
||||
margin: 8px 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
> div {
|
||||
display: grid;
|
||||
grid-template-columns: 48px minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
dt {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-1);
|
||||
line-height: 20px;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.warehouse-ship-popover__hint,
|
||||
.warehouse-ship-popover__empty {
|
||||
margin: 8px 0 0;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user