feat: 接口明细清空 Tab 与 Array 根去重,面单预览走鉴权流
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:
zzh 2026-09-11 19:14:26 +08:00
parent a24dcb41ff
commit becb942e5b
7 changed files with 507 additions and 37 deletions

View File

@ -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'), {

View File

@ -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>

View File

@ -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>

View File

@ -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
View 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('无法下载面单文件')
}

View File

@ -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) => {

View File

@ -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) => {