diff --git a/src/components/InterfaceDetail/index.vue b/src/components/InterfaceDetail/index.vue
index 3667508..4a13975 100644
--- a/src/components/InterfaceDetail/index.vue
+++ b/src/components/InterfaceDetail/index.vue
@@ -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,13 +2708,67 @@
try {
document.execCommand('copy');
Message.success('已复制到剪贴板');
+ return true;
} catch {
Message.error('复制失败');
+ return false;
+ } finally {
+ document.body.removeChild(textArea);
}
- 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() {
if (analysisBusy.value) return;
@@ -3656,6 +3707,14 @@
aria-label="Request content type"
@change="updateContentType"
/>
+
+ 复制
+
@@ -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 展示,导出剥离) */
diff --git a/src/locale/en-US.ts b/src/locale/en-US.ts
index 0eee2a6..9804187 100644
--- a/src/locale/en-US.ts
+++ b/src/locale/en-US.ts
@@ -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',
diff --git a/src/locale/zh-CN.ts b/src/locale/zh-CN.ts
index 6551e2f..926905b 100644
--- a/src/locale/zh-CN.ts
+++ b/src/locale/zh-CN.ts
@@ -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': '请先完善联系方式',
diff --git a/src/utils/jsonParameterTree.ts b/src/utils/jsonParameterTree.ts
index d140878..39bf2f2 100644
--- a/src/utils/jsonParameterTree.ts
+++ b/src/utils/jsonParameterTree.ts
@@ -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
();
+ list.forEach((r) => {
+ if (r?.id != null) byId.set(r.id, r);
+ });
+
+ const childrenOf = new Map();
+ 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`;
+}
diff --git a/src/views/h5/customer-center/components/H5PlaceOrderSheet.vue b/src/views/h5/customer-center/components/H5PlaceOrderSheet.vue
index accc0f9..4367001 100644
--- a/src/views/h5/customer-center/components/H5PlaceOrderSheet.vue
+++ b/src/views/h5/customer-center/components/H5PlaceOrderSheet.vue
@@ -231,6 +231,45 @@
{{ t('h5.placeOrder.inboundDoorPickupHint') }}
+
+
+
+ {{ t('h5.placeOrder.selfShipAddressTitle') }}
+
+ · {{ selectedWarehouseOption.label }}
+
+
+
+
+
+ {{ t('h5.placeOrder.selfShipSelectWarehouse') }}
+
+
+ {{ t('h5.placeOrder.selfShipAddressEmpty') }}
+
+
+
+ {{ t('h5.placeOrder.selfShipRecipient') }}
+ {{ selectedWarehouseOption.recipient || '-' }}
+
+
+ {{ t('h5.placeOrder.selfShipPhone') }}
+ {{ selectedWarehouseOption.contactPhone || '-' }}
+
+
+ {{ t('h5.placeOrder.selfShipAddress') }}
+ {{ selectedWarehouseOption.shippingAddress || '-' }}
+
+ {{ t('h5.placeOrder.selfShipAddressHint') }}
+
+