feat(wordaddin): port Word add-in taskpane from poc
พอร์ต taskpane ของ Word add-in "e-Court Documents Versioning" จาก D:\word-addin-poc มาเป็น sibling app ที่ route /docedit/wordaddin: - wordaddin/ (client-only zone): services พอร์ตดิบทั้งชุด (authService/versioningService เป็น mock รอ backend จริง) + models + config - components/wordaddin/: WordAddinGate ฉีด office.js self-host (singleton กัน StrictMode double-inject + fallback timer ครอบเคส onReady เงียบ) และ TaskpaneApp state machine แปลง JSX เป็น Genesis design system - scripts/fetch-office-js.mjs (postinstall ตัวที่สาม): ดาวน์โหลด Office.js runtime ล่าสุดจาก CDN Microsoft ไป public/wordaddin/office-js/ (gitignore สร้างใหม่ได้ — npm @microsoft/office-js ตกรุ่นไม่มี Document.close) - middleware/basePath/root page: เปิด /wordaddin เป็น public (auth แยกจาก แอป note), root /docedit redirect ไป taskpane ยกเว้นลิงก์ e-Court N8 (?fileUrl=&fileId=&typeId=) ที่ยังส่งต่อ /note ตามเดิม - docker/web.Dockerfile รัน fetch-office-js ตอน build + npm scripts dev:https สำหรับ sideload ตาม docs/WORDADDIN-DEV.md Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
7a146d48be
commit
c401f4deb7
37 changed files with 11216 additions and 17 deletions
128
wordaddin/services/fileService.ts
Normal file
128
wordaddin/services/fileService.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
/* global Office, console */
|
||||
|
||||
/**
|
||||
* ดึงไฟล์ .docx ปัจจุบันผ่าน File/Slice API (FR-2 ขั้นตอนที่ 5)
|
||||
* กติกา: Word เปิด File object พร้อมกันได้แค่ 2 ตัว → เรียก closeAsync เสมอใน finally
|
||||
* พอร์ตดิบจาก D:\word-addin-poc\addin\src\services\fileService.ts
|
||||
*/
|
||||
|
||||
export const DOCX_CONTENT_TYPE =
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
||||
|
||||
/** ขนาด slice ต่อครั้ง — 1MB กันกิน memory ยักษ์ในเครื่องรายงานน้อย */
|
||||
export const PDF_CONTENT_TYPE = "application/pdf";
|
||||
|
||||
const SLICE_SIZE = 1024 * 1024;
|
||||
|
||||
function officeError(result: Office.AsyncResult<unknown>, fallback: string): Error {
|
||||
return new Error(result.error?.message || fallback);
|
||||
}
|
||||
|
||||
function getFileAsync(fileType: Office.FileType): Promise<Office.File> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
Office.context.document.getFileAsync(fileType, { sliceSize: SLICE_SIZE }, (result) => {
|
||||
if (result.status === Office.AsyncResultStatus.Succeeded) {
|
||||
resolve(result.value);
|
||||
} else {
|
||||
reject(officeError(result, "เปิดไฟล์เอกสารไม่สำเร็จ"));
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error instanceof Error ? error : new Error("เปิดไฟล์เอกสารไม่สำเร็จ"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* host แต่ละตัวส่งข้อมูล slice มาคนละรูปแบบ (เอกสาร Office.js ระบุแค่ "byte array"):
|
||||
* - Word บน Windows ส่วนใหญ่ = ArrayBuffer / typed-array view
|
||||
* - Word บน Mac = base64 string หรือ byte array ธรรมดา (number[])
|
||||
* ปรับเป็น Uint8Array ให้หมดก่อนประกอบเป็น Blob
|
||||
*/
|
||||
export function sliceDataToBytes(data: unknown): Uint8Array<ArrayBuffer> {
|
||||
if (data instanceof ArrayBuffer) {
|
||||
return new Uint8Array(data);
|
||||
}
|
||||
if (ArrayBuffer.isView(data)) {
|
||||
const view = data as ArrayBufferView;
|
||||
return new Uint8Array(view.buffer as ArrayBuffer, view.byteOffset, view.byteLength);
|
||||
}
|
||||
if (typeof data === "string") {
|
||||
// base64 (บาง host ใส่ data-url prefix หรือขึ้นบรรทัดมาด้วย — ตัดทิ้งก่อน decode)
|
||||
const base64 = data.replace(/^data:[^,]*?;base64,/, "").replace(/\s/g, "");
|
||||
const binary = atob(base64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let index = 0; index < binary.length; index++) {
|
||||
bytes[index] = binary.charCodeAt(index);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
if (Array.isArray(data) && (data.length === 0 || typeof data[0] === "number")) {
|
||||
return Uint8Array.from(data);
|
||||
}
|
||||
throw new Error(`รูปแบบข้อมูลของ slice ไม่รองรับ (ได้รับ ${typeof data})`);
|
||||
}
|
||||
|
||||
/** อ่าน slice หนึ่งชิ้นแล้ว normalize เป็น Uint8Array */
|
||||
function getSliceAsync(file: Office.File, sliceIndex: number): Promise<BlobPart> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
file.getSliceAsync(sliceIndex, (result) => {
|
||||
if (result.status === Office.AsyncResultStatus.Succeeded) {
|
||||
try {
|
||||
resolve(sliceDataToBytes(result.value.data));
|
||||
} catch (error) {
|
||||
reject(
|
||||
error instanceof Error ? error : new Error(`อ่าน slice ที่ ${sliceIndex} ไม่สำเร็จ`)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
reject(officeError(result, `อ่าน slice ที่ ${sliceIndex} ไม่สำเร็จ`));
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error instanceof Error ? error : new Error(`อ่าน slice ที่ ${sliceIndex} ไม่สำเร็จ`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** ปิด File object — fail ก็แค่ warn ไม่ทับ error ของ flow หลัก */
|
||||
function closeFileQuietly(file: Office.File): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
file.closeAsync(() => resolve());
|
||||
} catch (error) {
|
||||
console.warn("closeAsync failed:", error);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ประกอบไฟล์ทั้งไฟล์เป็น Blob — อ่าน slice ทีละชิ้นตามลำดับ
|
||||
* เรียก closeAsync เสมอ (สำเร็จหรือ fail ก็ตาม)
|
||||
*/
|
||||
async function readFileAsBlob(fileType: Office.FileType, contentType: string): Promise<Blob> {
|
||||
const file = await getFileAsync(fileType);
|
||||
try {
|
||||
const chunks: BlobPart[] = [];
|
||||
for (let index = 0; index < file.sliceCount; index++) {
|
||||
chunks.push(await getSliceAsync(file, index));
|
||||
}
|
||||
return new Blob(chunks, { type: contentType });
|
||||
} finally {
|
||||
await closeFileQuietly(file);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCurrentDocumentFile(): Promise<Blob> {
|
||||
return readFileAsBlob(Office.FileType.Compressed, DOCX_CONTENT_TYPE);
|
||||
}
|
||||
|
||||
export async function getCurrentDocumentPdf(): Promise<Blob> {
|
||||
if (!Office.context.requirements.isSetSupported("PdfFile", "1.1")) {
|
||||
throw new Error("Word นี้ไม่รองรับการสร้าง PDF");
|
||||
}
|
||||
return readFileAsBlob(Office.FileType.Pdf, PDF_CONTENT_TYPE);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue