feat: 客户门户可折叠侧栏导航,去掉页签并区分打印入口提示 #11

Merged
zzh merged 6 commits from test into master 2026-10-24 19:13:04 +08:00
3 changed files with 52 additions and 24 deletions
Showing only changes of commit 95918100cf - Show all commits

View File

@ -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 限制;`/` 即为官网。
---

View File

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

View File

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