diff --git a/src/api/fileStorage.ts b/src/api/fileStorage.ts index 3675181..f9a6539 100644 --- a/src/api/fileStorage.ts +++ b/src/api/fileStorage.ts @@ -97,6 +97,17 @@ export async function getFileAccessUrl(key: string): Promise { return { key: storageKey, url } } +/** GET /file-storage/stream?key= — 鉴权流式读取(面单预览等) */ +export async function streamFileBlob(keyOrUrl: string): Promise { + const res = await axiosInstance.get(omsUrl('/file-storage/stream'), { + params: { key: keyOrUrl }, + responseType: 'blob', + timeout: 120000, + }) + if (res.data instanceof Blob) return res.data + return new Blob([res.data]) +} + /** POST /file-storage/normalize-key */ export async function normalizeFileStorageKey(pathOrUrl: string): Promise { const res = await axiosInstance.post(omsUrl('/file-storage/normalize-key'), { diff --git a/src/components/InterfaceDetail/index.vue b/src/components/InterfaceDetail/index.vue index be1ca93..5eaf70e 100644 --- a/src/components/InterfaceDetail/index.vue +++ b/src/components/InterfaceDetail/index.vue @@ -45,8 +45,10 @@ defaultResolveParameterType, extractCommentsFromJson, flatParameterListToTableRows, + hoistRedundantSyntheticArrayWrapper, parseJsonTextToParameterFlatList, pickBestJsonBodyText, + restoreSyntheticArrayWrapper, sortFlatParameterListParentFirst, } from '@/utils/jsonParameterTree'; @@ -603,19 +605,50 @@ pid === BODY_ROOT_VIRTUAL_ID ); } + /** 展示时提升掉的合成 array 包装,导出时还原 */ + const hoistedBodyArrayWrapper = ref(null); function stripBodyRootFromFlat(rows: any[]): any[] { const flat = flattenTreeTableRows(Array.isArray(rows) ? rows : []); return flat .filter((r) => !isBodyRootRow(r)) .map((r) => (r.pId === BODY_ROOT_VIRTUAL_ID ? { ...r, pId: 0 } : { ...r })); } - function ensureBodyRootInRows(rows: any[]): any[] { + function exportRequestBodyFlatParams(rows: any[]): any[] { + const stripped = stripBodyRootFromFlat(rows); + if (Number(props.basicFormData?.mappedParamType) !== ParameterType.Array) { + return stripped; + } + return restoreSyntheticArrayWrapper( + stripped, + hoistedBodyArrayWrapper.value, + isTopLevelBodyPid, + ); + } + function remapHoistedArrayActionRow(row: any) { + if (!row || isBodyRootRow(row) || row.pId !== BODY_ROOT_VIRTUAL_ID) return row; + const wrapper = hoistedBodyArrayWrapper.value; + if (wrapper && Number(props.basicFormData?.mappedParamType) === ParameterType.Array) { + return { ...row, pId: wrapper.id }; + } + return { ...row, pId: 0 }; + } + function ensureBodyRootInRows(rows: any[], options?: { hoistSyntheticArray?: boolean }): any[] { const cleaned = stripBodyRootFromFlat(rows); - if (!showRootMappedParamType.value) return cleaned; + if (!showRootMappedParamType.value) { + hoistedBodyArrayWrapper.value = null; + return cleaned; + } const rootType = props.basicFormData?.mappedParamType != null && props.basicFormData?.mappedParamType !== '' ? props.basicFormData.mappedParamType : ParameterType.Object; + const hoisted = hoistRedundantSyntheticArrayWrapper(cleaned, { + enabled: + Number(rootType) === ParameterType.Array && options?.hoistSyntheticArray !== false, + arrayType: ParameterType.Array, + isTopLevelPid: isTopLevelBodyPid, + }); + hoistedBodyArrayWrapper.value = hoisted.hoistedWrapper; const root = { id: BODY_ROOT_VIRTUAL_ID, pId: null, @@ -627,7 +660,7 @@ }; return [ root, - ...cleaned.map((r) => + ...hoisted.rows.map((r) => isTopLevelBodyPid(r.pId) ? { ...r, pId: BODY_ROOT_VIRTUAL_ID } : { ...r }, ), ]; @@ -647,10 +680,11 @@ const fromGrid = requestTableRef.value?.getTableData?.()?.fullData || requestTableRef.value?.xGrid?.getTableData()?.fullData; - return stripBodyRootFromFlat(getTreeTableFlatRows(fromGrid, requestTableProps.data)); + return exportRequestBodyFlatParams(getTreeTableFlatRows(fromGrid, requestTableProps.data)); } function stripBodyRootFromActionRows(actions: any) { - const filterRows = (arr: any[]) => (Array.isArray(arr) ? arr.filter((r) => !isBodyRootRow(r)) : []); + const filterRows = (arr: any[]) => + (Array.isArray(arr) ? arr.filter((r) => !isBodyRootRow(r)).map(remapHoistedArrayActionRow) : []); return { insertRecords: filterRows(actions?.insertRecords), removeRecords: filterRows(actions?.removeRecords), @@ -1239,6 +1273,35 @@ { key: 'Xml', label: 'XML' }, ]; + const currentRequestTabClearLabel = computed(() => { + if (requestTab.value === 'query') return 'Query 参数'; + if (requestTab.value === 'header') return 'Header 参数'; + if (requestTab.value === 'fixed') return '固定配置'; + if (requestTab.value === 'body') return 'Body 参数'; + if (requestTab.value === 'mappingBody') return 'Mapping Body 参数'; + if (requestTab.value === 'demo') return 'cURL 内容'; + return '当前 Tab'; + }); + + const showRequestTabClearAction = computed(() => { + if (requestTab.value === 'body' && isNoneBodyContentType.value) return false; + return ['query', 'header', 'fixed', 'body', 'mappingBody', 'demo'].includes(requestTab.value); + }); + + const canClearCurrentRequestTab = computed(() => { + if (!showRequestTabClearAction.value) return false; + if (requestTab.value === 'query') return queryTabCount.value > 0; + if (requestTab.value === 'header') return headerTabCount.value > 0; + if (requestTab.value === 'fixed') return fixedConfigTabCount.value > 0; + if (requestTab.value === 'body') { + if (isBodyKvMode.value) return !isBodyKvEmpty.value; + return !isBodyTreeEmpty.value; + } + if (requestTab.value === 'mappingBody') return !isMappingBodyEmpty.value; + if (requestTab.value === 'demo') return !!(requestExample.value || '').trim(); + return false; + }); + /** 当前 Request Tab 空态提示(展示在顶部 Tab 行正中) */ const requestHeaderEmptyTip = computed(() => { if (requestTab.value === 'query' && queryTabCount.value === 0) { @@ -1356,7 +1419,7 @@ if (!isDownstreamMode.value) return; const toMapping = isMappingBodyContentType(ct) && !isMappingBodyContentType(prev); if (!toMapping) return; - const requestFlat = stripBodyRootFromFlat(requestTableProps.data || []); + const requestFlat = getRequestBodyFlatParams(); const mappingFlat = flattenTreeTableRows(mappingBodyTableProps.data || []); if (requestFlat.length > 0 && mappingFlat.length < requestFlat.length) { await loadFlatListIntoTreeTable(requestFlat, mappingBodyTableProps, mappingBodyTableRef); @@ -1439,9 +1502,9 @@ { immediate: true }, ); - // Json/Xml 切换 / 首次进入:挂上或拆掉虚拟根节点 + // Json/Xml 切换 / 根类型变更:挂上、拆掉或提升虚拟根下的合成 array watch( - showRootMappedParamType, + [showRootMappedParamType, () => props.basicFormData?.mappedParamType], () => { applyRequestBodyTableData(getRequestBodyFlatParams()); }, @@ -1754,6 +1817,100 @@ return isTableRowEditing(row, bodyFormTableRef); } + async function removeTableRows(tableRef: { value?: { removeRow?: (row: any) => any; xGrid?: any } }, rows: any[]) { + if (!rows.length) return; + const grid = tableRef.value?.xGrid; + if (grid?.remove) { + await grid.remove(rows); + return; + } + for (const row of rows) { + await tableRef.value?.removeRow?.(row); + } + } + + function collectRemovableTableRows( + tableRef: any, + keepRow?: (row: any) => boolean, + ) { + const roots = [...(getTableFullData(tableRef) || [])]; + if (!keepRow) return roots; + const toRemove: any[] = []; + roots.forEach((row) => { + if (keepRow(row)) { + if (Array.isArray(row?.children) && row.children.length) { + toRemove.push(...row.children); + } + return; + } + toRemove.push(row); + }); + return toRemove; + } + + async function clearCurrentRequestTab() { + const tab = requestTab.value; + if (tab === 'query') { + await removeTableRows(queryTableRef, collectRemovableTableRows(queryTableRef)); + queryTableProps.data = []; + } else if (tab === 'header') { + await removeTableRows(headerTableRef, collectRemovableTableRows(headerTableRef)); + headerTableProps.data = []; + } else if (tab === 'fixed') { + await removeTableRows(queryFixedTableRef, collectRemovableTableRows(queryFixedTableRef)); + fixedParams.value = []; + fixedParamsHasChanges.value = true; + } else if (tab === 'body' && isBodyKvMode.value) { + await removeTableRows(bodyFormTableRef, collectRemovableTableRows(bodyFormTableRef)); + bodyFormTableProps.data = []; + } else if (tab === 'body' && !isNoneBodyContentType.value) { + if (hoistedBodyArrayWrapper.value) { + const persisted = getRequestBodyFlatParams(); + hoistedBodyArrayWrapper.value = null; + const rows = ensureBodyRootInRows(persisted, { hoistSyntheticArray: false }); + requestTableProps.data = rows; + await nextTick(); + await requestTableRef.value?.reloadData?.(rows); + await nextTick(); + } + await removeTableRows( + requestTableRef, + collectRemovableTableRows(requestTableRef, isBodyRootRow), + ); + hoistedBodyArrayWrapper.value = null; + const remaining = getTableFullData(requestTableRef, []); + requestTableProps.data = remaining.length ? remaining : ensureBodyRootInRows([]); + } else if (tab === 'mappingBody') { + await removeTableRows(mappingBodyTableRef, collectRemovableTableRows(mappingBodyTableRef)); + mappingBodyTableProps.data = []; + } else if (tab === 'demo') { + requestExample.value = ''; + } + markDirty(); + refreshTabCounts(); + } + + function confirmClearCurrentRequestTab() { + if (!canClearCurrentRequestTab.value) { + $baseMessage('当前 Tab 没有可清空的内容', 'warning'); + return; + } + const label = currentRequestTabClearLabel.value; + const content = + requestTab.value === 'fixed' + ? `确定清空当前「${label}」?清空后需点击「保存固定配置」才会同步到服务端。` + : requestTab.value === 'demo' + ? '确定清空当前 cURL 内容?' + : `确定清空当前「${label}」?此操作仅影响编辑区,保存接口后才会同步到服务端。`; + $baseConfirm({ + title: '清空确认', + content, + okText: '清空', + cancelText: '取消', + onOk: () => clearCurrentRequestTab(), + }); + } + // 新增 Query 参数 async function addNewQueryParam() { markDirty(); @@ -3175,6 +3332,15 @@ />
+ + 清空 + diff --git a/src/components/PrintTemplate/OrderPrintModal.vue b/src/components/PrintTemplate/OrderPrintModal.vue index 39e4ee4..2a6da5d 100644 --- a/src/components/PrintTemplate/OrderPrintModal.vue +++ b/src/components/PrintTemplate/OrderPrintModal.vue @@ -72,7 +72,13 @@
-