Phase 11: version PDF (FR-15)
- เลข version ใหม่: เว็บ-generate = v0 ฉบับตั้งต้น (ไม่มี PDF), add-in save (Start Versioning + Save & Version) = v1+ มี PDF เสมอ - PDF ผลิตจาก Word จริงผ่าน task pane เท่านั้น (getFileAsync Pdf + runtime guard, manifest ไม่เพิ่ม requirement); capture/upload เป็น best-effort — docx ผ่านแม้ PDF ล้ม + MessageBar เตือนตาม hasPdf - Backend: DocumentVersion.PdfObjectKey (nullable, migration AddVersionPdf) + sibling .pdf ใน bucket docx-versions · multipart part pdf (ผิด type → 400) · /download + /download-link รับ ?format=pdf (ไม่มี → 404) · presigned ฝังชื่อไฟล์ไทย + _vN (RFC 5987) - Web: เปิดไฟล์แทน blob-download — PDF แท็บใหม่, docx ผ่าน ms-word:ofe|u| พร้อม heuristic fallback ลิงก์สำรอง (useVersionOpen, versionDownload) · ปุ่ม "เปิดใน Word" ที่หน้า success ของฟอร์มสร้างเอกสาร - Spec v7 + CONTEXT.md + ADR 0001 + แผน docs/plans/version-pdf.md - Automated QA: backend 68 / add-in 63 / web 55 ผ่าน · manual acceptance ผ่านบน Word for Mac (2026-09-08) รวม fidelity + track changes — ค้างเฉพาะ Windows / Word on the web Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
618328ffa7
commit
39d610a781
39 changed files with 1387 additions and 212 deletions
75
web/app/composables/useVersionOpen.ts
Normal file
75
web/app/composables/useVersionOpen.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { onBeforeUnmount, ref } from 'vue'
|
||||
import { versionDownloadPath, wordOpenUrl, type VersionFormat } from '../utils/versionDownload'
|
||||
|
||||
/** Shared by history and generate success. Links stay available even if the focus heuristic fires. */
|
||||
export function useVersionOpen() {
|
||||
const { apiJson } = useApi()
|
||||
const openingVersionId = ref<string | null>(null)
|
||||
const downloadError = ref<string | null>(null)
|
||||
const fallbackUrl = ref<string | null>(null)
|
||||
const openNotice = ref<string | null>(null)
|
||||
let cleanup = () => {}
|
||||
let generation = 0
|
||||
let pendingTab: Window | null = null
|
||||
|
||||
function resetOpen() {
|
||||
generation++
|
||||
pendingTab?.close()
|
||||
pendingTab = null
|
||||
cleanup()
|
||||
openingVersionId.value = null
|
||||
downloadError.value = null
|
||||
fallbackUrl.value = null
|
||||
openNotice.value = null
|
||||
}
|
||||
|
||||
async function openVersion(documentId: string, versionId: string, format: VersionFormat = 'docx') {
|
||||
if (openingVersionId.value) return
|
||||
resetOpen()
|
||||
const current = generation
|
||||
openingVersionId.value = versionId
|
||||
// Reserve a tab during the user gesture, before the authenticated request loses activation.
|
||||
let tab: Window | null = null
|
||||
try {
|
||||
tab = format === 'pdf' ? window.open('about:blank', '_blank') : null
|
||||
if (tab) tab.opener = null
|
||||
pendingTab = tab
|
||||
const link = await apiJson<{ url: string; expiresAt: string }>(versionDownloadPath(documentId, versionId, format))
|
||||
if (current !== generation) { tab?.close(); return }
|
||||
if (!link) throw new Error('ขอลิงก์เปิดไฟล์ไม่สำเร็จ')
|
||||
fallbackUrl.value = link.url
|
||||
if (format === 'pdf') {
|
||||
if (tab && !tab.closed) {
|
||||
tab.location.replace(link.url)
|
||||
pendingTab = null
|
||||
}
|
||||
else openNotice.value = 'เบราว์เซอร์บล็อกแท็บใหม่ กรุณากดลิงก์เปิดไฟล์ด้านล่าง'
|
||||
} else {
|
||||
let switched = false
|
||||
const onBlur = () => { switched = true }
|
||||
const onVisibility = () => { if (document.hidden) switched = true }
|
||||
window.addEventListener('blur', onBlur)
|
||||
document.addEventListener('visibilitychange', onVisibility)
|
||||
const timer = window.setTimeout(() => {
|
||||
if (!switched) openNotice.value = 'หาก Word ไม่เปิด กรุณาดาวน์โหลดไฟล์แล้วเปิดใน Word'
|
||||
cleanup()
|
||||
}, 1500)
|
||||
cleanup = () => {
|
||||
window.clearTimeout(timer)
|
||||
window.removeEventListener('blur', onBlur)
|
||||
document.removeEventListener('visibilitychange', onVisibility)
|
||||
}
|
||||
window.location.assign(wordOpenUrl(link.url))
|
||||
}
|
||||
} catch (err) {
|
||||
tab?.close()
|
||||
if (pendingTab === tab) pendingTab = null
|
||||
if (current === generation) downloadError.value = err instanceof Error ? err.message : 'เปิดไฟล์ไม่สำเร็จ'
|
||||
} finally {
|
||||
if (current === generation) openingVersionId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(resetOpen)
|
||||
return { openingVersionId, downloadError, fallbackUrl, openNotice, openVersion, resetOpen }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue