From 95918100cfa6cd7b4e1ea7f18b7c04889482f8cf Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Fri, 23 Oct 2026 13:44:58 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=9D=A2=E5=8D=95=E9=A2=84=E8=A7=88?= =?UTF-8?q?=E4=BC=98=E5=85=88=20access-url=EF=BC=8C=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?=E5=99=A8=E8=A1=A5=E7=AE=B1=E5=AD=90=E6=80=BB=E6=95=B0=20mock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 生产预览先换新鲜签名直开,失败再 stream→blob;标签设计 mock 对齐 boxCount。 Co-authored-by: Cursor --- docs/前端页面设计规范.md | 2 +- src/utils/printPreview.ts | 73 +++++++++++++------ .../labelDesign/LabelDesignerPanel.vue | 1 + 3 files changed, 52 insertions(+), 24 deletions(-) diff --git a/docs/前端页面设计规范.md b/docs/前端页面设计规范.md index 0c6752e..92b6bdd 100644 --- a/docs/前端页面设计规范.md +++ b/docs/前端页面设计规范.md @@ -572,7 +572,7 @@ const { t } = useI18n(); 1. **前后端分域名**(方案 A):站点如 `ideship.com` / `test.ideship.com`,API 如 `api.ideship.com` / `api.test.ideship.com`。浏览器直连 API,前端容器不做 Admin 反代,也无 `/__cos__` 反代。 2. SPA 回退由 `serve -s`(或等价静态服务器)处理;刷新 `/order/batchJobs` 等路径应回 `index.html`,不依赖 nginx `try_files`。 3. 官网 **`/`** 为 Vue 路由(`src/views/site`);静态资源在 **`/site/`**;**`/9610.html`** 为 `public/9610.html`。客户登录 **`/login`**,管理端 **`/admin`**。 -4. 面单/附件预览:开发环境可用 Vite `/__cos__`;**生产**经鉴权 `file-storage/stream` → blob,勿直开 `https://cos.*`(自定义域证书常不匹配)。 +4. 面单/附件预览:开发可用 Vite `/__cos__`;生产优先 `file-storage/access-url` 新鲜签名直开,失败再 `stream` → blob(勿依赖过期库内签名 URL)。 5. 本地 `pnpm dev` 不受 Ingress 限制;`/` 即为官网。 --- diff --git a/src/utils/printPreview.ts b/src/utils/printPreview.ts index d758a37..ba1e423 100644 --- a/src/utils/printPreview.ts +++ b/src/utils/printPreview.ts @@ -1,5 +1,5 @@ import { axiosInstance } from '@/api/rbac' -import { normalizeFileStorageKey, streamFileBlob } from '@/api/fileStorage' +import { getFileAccessUrl, normalizeFileStorageKey, streamFileBlob } from '@/api/fileStorage' import { encodeCosSignedQuery, extractOmsRequestError, @@ -432,10 +432,49 @@ export async function fetchPrintAssetBytes(rawUrl: string): Promise return new Uint8Array(await blob.arrayBuffer()) } +/** + * 经 /file-storage/access-url 换新鲜可直开地址(重新签名)。 + * 返回非 http(s)(如 Local 降级 key)时视为失败,由调用方走 stream→blob。 + */ +async function resolveFreshAccessUrl(rawUrl: string): Promise { + const keys = await buildPrintStreamKeyCandidates(rawUrl) + if (!keys.length) return null + + // 优先 object key,再试完整签名 URL(后端会剥签重签) + const ordered = [ + ...keys.filter((k) => !/^https?:\/\//i.test(k)), + ...keys.filter((k) => /^https?:\/\//i.test(k)), + ] + + for (const key of ordered) { + try { + const { url } = await getFileAccessUrl(key) + const trimmed = String(url || '').trim() + if (/^https?:\/\//i.test(trimmed)) return trimmed + } catch { + // 试下一个候选 + } + } + return null +} + +function writePreviewStatus(win: Window, title: string, bodyHtml: string, color = '#666') { + try { + win.document.open() + win.document.write( + `${escapeHtml(title)}` + + `${bodyHtml}`, + ) + win.document.close() + } catch { + // 跨域或 document 不可写时忽略 + } +} + /** * 新标签页预览面单/附件。 * - 开发 + COS:同步打开 Vite `/__cos__/…`(`;`→`%3B`)。 - * - 生产:无站内反代,一律 file-storage/stream → blob(避免直开 cos 证书告警)。 + * - 其余:access-url 新鲜签名直开;失败再 file-storage/stream → blob。 */ export async function openPrintAssetInNewTab( rawUrl: string, @@ -458,40 +497,28 @@ export async function openPrintAssetInNewTab( } } - // 同步开页,避免 await 后被当成非用户手势拦截;失败时在页内写错误,不直接 close + // 同步开页,避免 await 后被当成非用户手势拦截 const win = window.open('about:blank', '_blank') if (!win) { throw new Error('请允许浏览器弹窗以预览文件') } const title = options?.loadingTitle || '加载中…' - try { - win.document.open() - win.document.write( - `${escapeHtml(title)}` + - `${escapeHtml(title)}…`, - ) - win.document.close() - } catch { - // 个别环境写 document 失败时仍继续拉 blob - } + writePreviewStatus(win, title, `${escapeHtml(title)}…`) try { + const accessUrl = await resolveFreshAccessUrl(raw) + if (accessUrl) { + win.location.href = accessUrl + return + } + const blob = await fetchPreviewBlob(raw) const blobUrl = URL.createObjectURL(blob) win.location.href = blobUrl window.setTimeout(() => URL.revokeObjectURL(blobUrl), 120_000) } catch (e) { const msg = e instanceof Error ? e.message : String(e || '预览失败') - try { - win.document.open() - win.document.write( - `预览失败` + - `${escapeHtml(msg)}`, - ) - win.document.close() - } catch { - // ignore - } + writePreviewStatus(win, '预览失败', escapeHtml(msg), '#c00') throw e } } diff --git a/src/views/channel/labelDesign/LabelDesignerPanel.vue b/src/views/channel/labelDesign/LabelDesignerPanel.vue index 8321764..dcb551f 100644 --- a/src/views/channel/labelDesign/LabelDesignerPanel.vue +++ b/src/views/channel/labelDesign/LabelDesignerPanel.vue @@ -111,6 +111,7 @@ const MOCK_BY_FIELD: Record = { markReference: 'SF1234567890', markBoxNo: 'BOX-001', boxSeqNo: '1', + boxCount: '2', barcode: 'BOX-001', boxIndexText: '1/2', skuSummary: 'SKU001 T-Shirt×2\nSKU002 Pants×1',