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',