feat: 接口明细清空 Tab 与 Array 根去重,面单预览走鉴权流
All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m39s
All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m39s
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a24dcb41ff
commit
becb942e5b
@ -97,6 +97,17 @@ export async function getFileAccessUrl(key: string): Promise<AccessUrlResult> {
|
||||
return { key: storageKey, url }
|
||||
}
|
||||
|
||||
/** GET /file-storage/stream?key= — 鉴权流式读取(面单预览等) */
|
||||
export async function streamFileBlob(keyOrUrl: string): Promise<Blob> {
|
||||
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<string | null> {
|
||||
const res = await axiosInstance.post(omsUrl('/file-storage/normalize-key'), {
|
||||
|
||||
@ -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<any | null>(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 @@
|
||||
/>
|
||||
<div class="section-header-right">
|
||||
<div class="section-actions">
|
||||
<a-button
|
||||
v-if="showRequestTabClearAction"
|
||||
size="small"
|
||||
:disabled="!canClearCurrentRequestTab"
|
||||
title="清空当前 Tab 内容"
|
||||
@click="confirmClearCurrentRequestTab"
|
||||
>
|
||||
清空
|
||||
</a-button>
|
||||
<template v-if="requestTab === 'query'">
|
||||
<a-button size="small" @click="addNewQueryParam">新增</a-button>
|
||||
</template>
|
||||
|
||||
@ -72,7 +72,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="batch-preview">
|
||||
<iframe v-if="activeItem?.url" ref="previewFrameRef" class="print-preview-frame" :src="activeItem.url" />
|
||||
<div v-if="previewLoading" class="print-preview-loading">面单加载中…</div>
|
||||
<iframe
|
||||
v-else-if="previewSrc"
|
||||
ref="previewFrameRef"
|
||||
class="print-preview-frame"
|
||||
:src="previewSrc"
|
||||
/>
|
||||
<a-empty v-else description="暂无可预览打印内容" />
|
||||
</div>
|
||||
</div>
|
||||
@ -85,7 +91,13 @@
|
||||
<span class="print-preview-title">PDF/HTML 预览</span>
|
||||
<a-button size="small" @click="openActiveInNewTab">新标签打开</a-button>
|
||||
</div>
|
||||
<iframe v-if="activeItem?.url" ref="previewFrameRef" class="print-preview-frame" :src="activeItem.url" />
|
||||
<div v-if="previewLoading" class="print-preview-loading">面单加载中…</div>
|
||||
<iframe
|
||||
v-else-if="previewSrc"
|
||||
ref="previewFrameRef"
|
||||
class="print-preview-frame"
|
||||
:src="previewSrc"
|
||||
/>
|
||||
<a-empty v-else description="暂无可预览打印内容" />
|
||||
</section>
|
||||
<aside class="single-side-panel">
|
||||
@ -125,7 +137,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import {
|
||||
fetchHtmlTextForHiprint,
|
||||
@ -135,6 +147,7 @@ import {
|
||||
} from '@/utils/hiprint/hiprintElectronSocket';
|
||||
import { useHiprintClientState } from '@/utils/hiprint/hiprintClientState';
|
||||
import { resolveOmsAssetUrl } from '@/api/oms/common';
|
||||
import { fetchPrintAssetBytes, resolvePrintPreviewSrc } from '@/utils/printPreview';
|
||||
import {
|
||||
buildHiprintBlobPrintExtras,
|
||||
HIPRINT_PAPER_PRESET_OPTIONS,
|
||||
@ -169,34 +182,94 @@ const hiprintClientState = useHiprintClientState();
|
||||
const previewFrameRef = ref<HTMLIFrameElement>();
|
||||
const activeId = ref<number | string | null>(null);
|
||||
const hiprintForm = ref(readHiprintPrintFormFromStorage());
|
||||
const previewSrc = ref('');
|
||||
const previewLoading = ref(false);
|
||||
let previewBlobUrl: string | null = null;
|
||||
let previewLoadToken = 0;
|
||||
|
||||
const printableItems = computed(() => (props.items || []).filter((x) => !!x.url));
|
||||
const printableItems = computed(() => (props.items || []).filter((x) => !!String(x?.url || '').trim()));
|
||||
const activeItem = computed(() => {
|
||||
if (!printableItems.value.length) return null;
|
||||
if (activeId.value == null) return printableItems.value[0];
|
||||
return printableItems.value.find((x) => x.id === activeId.value) || printableItems.value[0];
|
||||
});
|
||||
|
||||
function revokePreviewBlob() {
|
||||
if (!previewBlobUrl) return;
|
||||
URL.revokeObjectURL(previewBlobUrl);
|
||||
previewBlobUrl = null;
|
||||
}
|
||||
|
||||
async function loadPreviewForActive() {
|
||||
const token = ++previewLoadToken;
|
||||
revokePreviewBlob();
|
||||
previewSrc.value = '';
|
||||
if (!props.visible || !activeItem.value?.url) {
|
||||
previewLoading.value = false;
|
||||
return;
|
||||
}
|
||||
previewLoading.value = true;
|
||||
try {
|
||||
const src = await resolvePrintPreviewSrc(activeItem.value.url);
|
||||
if (token !== previewLoadToken) {
|
||||
if (src.startsWith('blob:')) URL.revokeObjectURL(src);
|
||||
return;
|
||||
}
|
||||
if (src.startsWith('blob:')) {
|
||||
previewBlobUrl = src;
|
||||
}
|
||||
previewSrc.value = src;
|
||||
} catch {
|
||||
if (token !== previewLoadToken) return;
|
||||
previewSrc.value = toAbsoluteUrl(activeItem.value.url);
|
||||
} finally {
|
||||
if (token === previewLoadToken) previewLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (!visible) return;
|
||||
if (!visible) {
|
||||
revokePreviewBlob();
|
||||
previewSrc.value = '';
|
||||
previewLoading.value = false;
|
||||
return;
|
||||
}
|
||||
activeId.value = printableItems.value[0]?.id ?? null;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
[
|
||||
() => props.visible,
|
||||
() => props.items,
|
||||
() => activeItem.value?.id,
|
||||
() => activeItem.value?.url,
|
||||
],
|
||||
() => {
|
||||
void loadPreviewForActive();
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => hiprintForm.value,
|
||||
(val) => writeHiprintPrintFormToStorage(val),
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
previewLoadToken += 1;
|
||||
revokePreviewBlob();
|
||||
});
|
||||
|
||||
function openActiveInNewTab() {
|
||||
if (!activeItem.value?.url) {
|
||||
Message.warning('当前无可打开的打印链接');
|
||||
return;
|
||||
}
|
||||
window.open(activeItem.value.url, '_blank', 'noopener,noreferrer');
|
||||
window.open(toAbsoluteUrl(activeItem.value.url) || activeItem.value.url, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
|
||||
function toAbsoluteUrl(url: string) {
|
||||
@ -211,7 +284,20 @@ function toAbsoluteUrl(url: string) {
|
||||
|
||||
async function silentPrintByUrl(url: string) {
|
||||
const extras = buildHiprintBlobPrintExtras(hiprintForm.value);
|
||||
if (/\.pdf(\?|#|$)/i.test(url)) {
|
||||
const isPdf = /\.pdf(\?|#|$)/i.test(url);
|
||||
try {
|
||||
const bytes = await fetchPrintAssetBytes(url);
|
||||
if (isPdf) {
|
||||
await hiprintSendBlobPdf({ pdf_blob: bytes, printExtras: extras });
|
||||
return;
|
||||
}
|
||||
const html = new TextDecoder('utf-8').decode(bytes);
|
||||
await hiprintSendHtmlPrint({ html, printExtras: extras });
|
||||
return;
|
||||
} catch {
|
||||
// 回退旧逻辑
|
||||
}
|
||||
if (isPdf) {
|
||||
const pdf_blob = await fetchPdfBytesForHiprint(toAbsoluteUrl(url));
|
||||
await hiprintSendBlobPdf({ pdf_blob, printExtras: extras });
|
||||
return;
|
||||
@ -245,7 +331,7 @@ async function printActive() {
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
window.open(activeItem.value.url, '_blank', 'noopener,noreferrer');
|
||||
window.open(toAbsoluteUrl(activeItem.value.url) || activeItem.value.url, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
|
||||
async function printAll() {
|
||||
@ -259,7 +345,7 @@ async function printAll() {
|
||||
}
|
||||
const tasks = printableItems.value.map((item) =>
|
||||
silentPrintByUrl(item.url).catch(() => {
|
||||
window.open(item.url, '_blank', 'noopener,noreferrer');
|
||||
window.open(toAbsoluteUrl(item.url) || item.url, '_blank', 'noopener,noreferrer');
|
||||
})
|
||||
);
|
||||
const results = await Promise.allSettled(tasks);
|
||||
@ -323,4 +409,14 @@ async function printAll() {
|
||||
.print-preview-title { font-size: 12px; font-weight: 600; color: var(--color-text-2); }
|
||||
.single-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: auto; }
|
||||
.print-preview-frame { width: 100%; height: 100%; min-height: 640px; border: none; }
|
||||
.print-preview-loading {
|
||||
flex: 1;
|
||||
min-height: 640px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-text-3);
|
||||
font-size: 13px;
|
||||
background: var(--color-fill-1);
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -43,6 +43,83 @@ function defaultIdFactory(): number {
|
||||
/** 解析树中的占位节点名,不参与 // 注释路径拼接 */
|
||||
export const SYNTHETIC_PARAM_NAMES = new Set(['obj', 'val', 'array']);
|
||||
|
||||
function pickArrayWrapperPersistFields(row: any) {
|
||||
return {
|
||||
id: row.id,
|
||||
pId: 0,
|
||||
name: row.name,
|
||||
type: row.type,
|
||||
description: row.description ?? '',
|
||||
isRequired: !!row.isRequired,
|
||||
alias: row.alias || row.name,
|
||||
cnName: row.cnName ?? '',
|
||||
optionType: row.optionType ?? 1,
|
||||
};
|
||||
}
|
||||
|
||||
/** JSON 数组根解析出的合成 array 节点(与虚拟根 Array 类型重复) */
|
||||
export function isSyntheticArrayWrapper(row: any, arrayType: number = ParameterType.Array): boolean {
|
||||
if (!row) return false;
|
||||
const name = String(row.name ?? row.Name ?? '').trim().toLowerCase();
|
||||
return name === 'array' && Number(row.type ?? row.Type) === Number(arrayType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 虚拟根已是 Array 时,去掉顶层合成 array 包装,把其子节点提到根下。
|
||||
* 避免类型列连续出现 Array / Array。导出时按 childIds 还原,其它顶层兄弟不动。
|
||||
*/
|
||||
export function hoistRedundantSyntheticArrayWrapper(
|
||||
rows: any[],
|
||||
options: {
|
||||
enabled: boolean;
|
||||
arrayType?: number;
|
||||
isTopLevelPid: (pId: any) => boolean;
|
||||
},
|
||||
): { rows: any[]; hoistedWrapper: any | null } {
|
||||
const list = Array.isArray(rows) ? rows : [];
|
||||
if (!options.enabled) return { rows: list, hoistedWrapper: null };
|
||||
|
||||
const arrayType = options.arrayType ?? ParameterType.Array;
|
||||
const topLevel = list.filter((r) => r && options.isTopLevelPid(r.pId ?? r.PId));
|
||||
const wrappers = topLevel.filter((r) => isSyntheticArrayWrapper(r, arrayType));
|
||||
if (!wrappers.length) return { rows: list, hoistedWrapper: null };
|
||||
|
||||
const wrapper =
|
||||
wrappers.find((w) => list.some((r) => r && (r.pId ?? r.PId) === (w.id ?? w.Id))) || wrappers[0];
|
||||
const wrapperId = wrapper.id ?? wrapper.Id;
|
||||
const childIds = list
|
||||
.filter((r) => r && (r.pId ?? r.PId) === wrapperId)
|
||||
.map((r) => r.id ?? r.Id)
|
||||
.filter((id) => id != null);
|
||||
if (!childIds.length) return { rows: list, hoistedWrapper: null };
|
||||
|
||||
return {
|
||||
rows: list
|
||||
.filter((r) => r && (r.id ?? r.Id) !== wrapperId)
|
||||
.map((r) => ((r.pId ?? r.PId) === wrapperId ? { ...r, pId: 0 } : r)),
|
||||
hoistedWrapper: { ...pickArrayWrapperPersistFields(wrapper), childIds },
|
||||
};
|
||||
}
|
||||
|
||||
/** 把展示时提升掉的合成 array 包装还原回扁平列表(保存 / 校验) */
|
||||
export function restoreSyntheticArrayWrapper(
|
||||
rows: any[],
|
||||
wrapper: any | null,
|
||||
_isTopLevelPid: (pId: any) => boolean,
|
||||
): any[] {
|
||||
const list = Array.isArray(rows) ? rows : [];
|
||||
if (!wrapper) return list;
|
||||
const wrapperId = wrapper.id ?? wrapper.Id;
|
||||
if (wrapperId == null || list.some((r) => (r?.id ?? r?.Id) === wrapperId)) return list;
|
||||
const { childIds, ...wrapperRow } = wrapper;
|
||||
const childIdSet = new Set((childIds || []).map((id: any) => id));
|
||||
if (!childIdSet.size) return list;
|
||||
return [
|
||||
{ ...wrapperRow, pId: 0 },
|
||||
...list.map((r) => (childIdSet.has(r?.id ?? r?.Id) ? { ...r, pId: wrapperId } : r)),
|
||||
];
|
||||
}
|
||||
|
||||
type JsonPathFrame = { kind: 'object' | 'array'; key?: string };
|
||||
|
||||
/**
|
||||
|
||||
130
src/utils/printPreview.ts
Normal file
130
src/utils/printPreview.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import { axiosInstance } from '@/api/rbac'
|
||||
import { omsUrl, resolveOmsAssetUrl } from '@/api/oms/common'
|
||||
import { normalizeFileStorageKey } from '@/api/fileStorage'
|
||||
|
||||
function guessMime(pathOrUrl: string): string {
|
||||
if (/\.pdf(\?|#|$)/i.test(pathOrUrl)) return 'application/pdf'
|
||||
if (/\.html?(\?|#|$)/i.test(pathOrUrl)) return 'text/html; charset=utf-8'
|
||||
return 'application/octet-stream'
|
||||
}
|
||||
|
||||
/** 开发环境把生产 Admin 绝对地址改写成 Vite 同源代理路径 */
|
||||
function rewriteDevAdminAssetPath(url: string): string {
|
||||
if (!import.meta.env.DEV) return url
|
||||
const trimmed = String(url || '').trim()
|
||||
if (!trimmed) return trimmed
|
||||
const proxied = trimmed.replace(/^https?:\/\/api\.(?:ideship|nanafa)\.com\/admin/i, '')
|
||||
return proxied || trimmed
|
||||
}
|
||||
|
||||
function toSameOriginPath(url: string): string {
|
||||
const resolved = rewriteDevAdminAssetPath(resolveOmsAssetUrl(url) || url)
|
||||
if (!resolved) return ''
|
||||
if (/^https?:\/\//i.test(resolved)) return resolved
|
||||
return resolved.startsWith('/') ? resolved : `/${resolved}`
|
||||
}
|
||||
|
||||
async function fetchBlobViaStream(keyOrUrl: string): Promise<Blob> {
|
||||
const res = await axiosInstance.get(omsUrl('/file-storage/stream'), {
|
||||
params: { key: keyOrUrl },
|
||||
responseType: 'blob',
|
||||
timeout: 120000,
|
||||
})
|
||||
const type = String(res.headers['content-type'] || guessMime(keyOrUrl))
|
||||
if (res.data instanceof Blob) {
|
||||
return res.data.type ? res.data : new Blob([res.data], { type })
|
||||
}
|
||||
return new Blob([res.data], { type })
|
||||
}
|
||||
|
||||
async function fetchBlobViaSameOriginPath(path: string): Promise<Blob> {
|
||||
const res = await axiosInstance.get(path, {
|
||||
responseType: 'blob',
|
||||
timeout: 120000,
|
||||
})
|
||||
const type = String(res.headers['content-type'] || guessMime(path))
|
||||
if (res.data instanceof Blob) {
|
||||
return res.data.type ? res.data : new Blob([res.data], { type })
|
||||
}
|
||||
return new Blob([res.data], { type })
|
||||
}
|
||||
|
||||
/**
|
||||
* 将打印 URL 转为可在 iframe 中预览的 blob: 地址(同源,避免 COS / api 跨域空白)。
|
||||
* 失败时回退为可直链打开的绝对地址。
|
||||
*/
|
||||
export async function resolvePrintPreviewSrc(rawUrl: string): Promise<string> {
|
||||
const raw = String(rawUrl || '').trim()
|
||||
if (!raw) return ''
|
||||
|
||||
const candidates = new Set<string>()
|
||||
candidates.add(raw)
|
||||
const asset = resolveOmsAssetUrl(raw)
|
||||
if (asset) candidates.add(asset)
|
||||
candidates.add(rewriteDevAdminAssetPath(raw))
|
||||
if (asset) candidates.add(rewriteDevAdminAssetPath(asset))
|
||||
|
||||
try {
|
||||
const key = await normalizeFileStorageKey(raw)
|
||||
if (key) candidates.add(key)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const value = String(candidate || '').trim()
|
||||
if (!value) continue
|
||||
try {
|
||||
const blob = await fetchBlobViaStream(value)
|
||||
return URL.createObjectURL(blob)
|
||||
} catch {
|
||||
// try next
|
||||
}
|
||||
}
|
||||
|
||||
const sameOriginPath = toSameOriginPath(raw)
|
||||
if (sameOriginPath.startsWith('/')) {
|
||||
try {
|
||||
const blob = await fetchBlobViaSameOriginPath(sameOriginPath)
|
||||
return URL.createObjectURL(blob)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return `${window.location.origin}${sameOriginPath}`
|
||||
}
|
||||
|
||||
return sameOriginPath
|
||||
}
|
||||
|
||||
/** 供 hiprint 静默打印:优先经 /file-storage/stream 拉取字节 */
|
||||
export async function fetchPrintAssetBytes(rawUrl: string): Promise<Uint8Array> {
|
||||
const raw = String(rawUrl || '').trim()
|
||||
if (!raw) throw new Error('打印地址为空')
|
||||
|
||||
const candidates = new Set<string>([raw])
|
||||
try {
|
||||
const key = await normalizeFileStorageKey(raw)
|
||||
if (key) candidates.add(key)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const value = String(candidate || '').trim()
|
||||
if (!value) continue
|
||||
try {
|
||||
const blob = await fetchBlobViaStream(value)
|
||||
return new Uint8Array(await blob.arrayBuffer())
|
||||
} catch {
|
||||
// try next
|
||||
}
|
||||
}
|
||||
|
||||
const path = toSameOriginPath(raw)
|
||||
if (path.startsWith('/')) {
|
||||
const blob = await fetchBlobViaSameOriginPath(path)
|
||||
return new Uint8Array(await blob.arrayBuffer())
|
||||
}
|
||||
|
||||
throw new Error('无法下载面单文件')
|
||||
}
|
||||
@ -1263,10 +1263,6 @@
|
||||
Message.warning('请选择要打印的订单');
|
||||
return;
|
||||
}
|
||||
if (!printConnectionState.enabled || !printConnectionState.connected) {
|
||||
Message.warning('请先在顶部打开并连接打印开关');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await withOrderActionLoading('singlePrint', async () => {
|
||||
const { printUrl } = await postOrderPrint({ id: getOrderRowId(row)! });
|
||||
@ -1277,6 +1273,9 @@
|
||||
}
|
||||
singlePrintItems.value = [buildPrintItem(row, printUrl)];
|
||||
singlePrintVisible.value = true;
|
||||
if (!printConnectionState.enabled || !printConnectionState.connected) {
|
||||
Message.info('已打开面单预览;静默打印需先在顶部连接打印开关');
|
||||
}
|
||||
});
|
||||
} catch (e: any) {
|
||||
if (e?.message !== 'busy') {
|
||||
@ -1290,10 +1289,6 @@
|
||||
batchPrintItems.value = [];
|
||||
const rows = requireSelectedRows();
|
||||
if (!rows.length) return;
|
||||
if (!printConnectionState.enabled || !printConnectionState.connected) {
|
||||
Message.warning('请先在顶部打开并连接打印开关');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await withOrderActionLoading('batchPrint', async () => {
|
||||
await runSelectedOrderBatch({
|
||||
@ -1643,7 +1638,7 @@
|
||||
() => activeTab.value === 'pendingPayment' && hasPermission('Order.MyOrders.Pay'),
|
||||
);
|
||||
|
||||
const actionRenderer = () => {
|
||||
const actionRenderer = (_rowData: any, rowIndex: number) => {
|
||||
const parts = [
|
||||
renderWarehouseActionBtn('view', t('common.viewDetail'), 'view'),
|
||||
renderWarehouseActionBtn('track', t('order.myOrders.actions.viewTrack'), 'track'),
|
||||
@ -1655,7 +1650,7 @@
|
||||
if (showRowPayAction.value) {
|
||||
parts.push(renderWarehouseActionBtn('pay', t('order.myOrders.actions.pay'), 'pay'));
|
||||
}
|
||||
return wrapWarehouseActions(parts);
|
||||
return wrapWarehouseActions(parts, rowIndex);
|
||||
};
|
||||
|
||||
const customCellRenderer = (row: number, col: number, value: any, rowData: any) => {
|
||||
|
||||
@ -1218,10 +1218,6 @@
|
||||
Message.warning('请选择要打印的订单');
|
||||
return;
|
||||
}
|
||||
if (!printConnectionState.enabled || !printConnectionState.connected) {
|
||||
Message.warning('请先在顶部打开并连接打印开关');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await withOrderActionLoading('singlePrint', async () => {
|
||||
const { printUrl } = await postOrderPrint({ id: getOrderRowId(row)! });
|
||||
@ -1232,6 +1228,9 @@
|
||||
}
|
||||
singlePrintItems.value = [buildPrintItem(row, printUrl)];
|
||||
singlePrintVisible.value = true;
|
||||
if (!printConnectionState.enabled || !printConnectionState.connected) {
|
||||
Message.info('已打开面单预览;静默打印需先在顶部连接打印开关');
|
||||
}
|
||||
});
|
||||
} catch (e: any) {
|
||||
if (e?.message !== 'busy') {
|
||||
@ -1245,10 +1244,6 @@
|
||||
batchPrintItems.value = [];
|
||||
const rows = requireSelectedRows();
|
||||
if (!rows.length) return;
|
||||
if (!printConnectionState.enabled || !printConnectionState.connected) {
|
||||
Message.warning('请先在顶部打开并连接打印开关');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await withOrderActionLoading('batchPrint', async () => {
|
||||
await runSelectedOrderBatch({
|
||||
@ -1564,7 +1559,7 @@
|
||||
() => activeTab.value === 'pendingPayment' && hasPermission('Order.WaybillManage.Pay'),
|
||||
);
|
||||
|
||||
const actionRenderer = () => {
|
||||
const actionRenderer = (_rowData: any, rowIndex: number) => {
|
||||
const parts = [
|
||||
renderWarehouseActionBtn('view', t('common.viewDetail'), 'view'),
|
||||
renderWarehouseActionBtn('track', t('order.myOrders.actions.viewTrack'), 'track'),
|
||||
@ -1576,7 +1571,7 @@
|
||||
if (showRowPayAction.value) {
|
||||
parts.push(renderWarehouseActionBtn('pay', t('order.myOrders.actions.pay'), 'pay'));
|
||||
}
|
||||
return wrapWarehouseActions(parts);
|
||||
return wrapWarehouseActions(parts, rowIndex);
|
||||
};
|
||||
|
||||
const customCellRenderer = (row: number, col: number, value: any, rowData: any) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user