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" /> + + 复制 +
@@ -4428,6 +4487,14 @@ aria-label="Response content type" @change="updateResponseContentType" /> + + 复制 +
@@ -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') }} + + + +
+

+ {{ t('h5.placeOrder.selfShipSelectWarehouse') }} +

+ + +