fix: 面单预览优先 access-url,设计器补箱子总数 mock
All checks were successful
Build and Deploy Front / deploy (push) Successful in 6m52s
All checks were successful
Build and Deploy Front / deploy (push) Successful in 6m52s
生产预览先换新鲜签名直开,失败再 stream→blob;标签设计 mock 对齐 boxCount。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
c6723ab476
commit
95918100cf
@ -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 限制;`/` 即为官网。
|
||||
|
||||
---
|
||||
|
||||
@ -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<Uint8Array>
|
||||
return new Uint8Array(await blob.arrayBuffer())
|
||||
}
|
||||
|
||||
/**
|
||||
* 经 /file-storage/access-url 换新鲜可直开地址(重新签名)。
|
||||
* 返回非 http(s)(如 Local 降级 key)时视为失败,由调用方走 stream→blob。
|
||||
*/
|
||||
async function resolveFreshAccessUrl(rawUrl: string): Promise<string | null> {
|
||||
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(
|
||||
`<!doctype html><title>${escapeHtml(title)}</title>` +
|
||||
`<body style="font:14px/1.5 sans-serif;padding:24px;color:${color}">${bodyHtml}</body>`,
|
||||
)
|
||||
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(
|
||||
`<!doctype html><title>${escapeHtml(title)}</title>` +
|
||||
`<body style="font:14px/1.5 sans-serif;padding:24px;color:#666">${escapeHtml(title)}…</body>`,
|
||||
)
|
||||
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(
|
||||
`<!doctype html><title>预览失败</title>` +
|
||||
`<body style="font:14px/1.5 sans-serif;padding:24px;color:#c00">${escapeHtml(msg)}</body>`,
|
||||
)
|
||||
win.document.close()
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
writePreviewStatus(win, '预览失败', escapeHtml(msg), '#c00')
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,6 +111,7 @@ const MOCK_BY_FIELD: Record<string, string> = {
|
||||
markReference: 'SF1234567890',
|
||||
markBoxNo: 'BOX-001',
|
||||
boxSeqNo: '1',
|
||||
boxCount: '2',
|
||||
barcode: 'BOX-001',
|
||||
boxIndexText: '1/2',
|
||||
skuSummary: 'SKU001 T-Shirt×2\nSKU002 Pants×1',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user