From fbe3b62a95ce5e24c62c4c084b99da3af432f7ef Mon Sep 17 00:00:00 2001 From: "DESKTOP-1R2VSQH\\Lenovo ThinkPad E490" Date: Fri, 18 Jul 2025 15:06:41 +0700 Subject: [PATCH] load orgchart --- package-lock.json | 50 +++++++++ package.json | 2 + src/modules/12_organization/views/main.vue | 54 ++++++---- src/plugins/exportChart.ts | 115 +++++++++++++++++++++ src/types/dom-to-image-more.d.ts | 36 +++++++ 5 files changed, 237 insertions(+), 20 deletions(-) create mode 100644 src/plugins/exportChart.ts create mode 100644 src/types/dom-to-image-more.d.ts diff --git a/package-lock.json b/package-lock.json index 91582e9..c62a2dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,8 +19,10 @@ "@tato30/vue-pdf": "^1.5.1", "@vuepic/vue-datepicker": "^3.6.3", "bma-org-chart": "^0.0.7", + "dom-to-image-more": "^3.6.0", "keycloak-js": "^20.0.2", "moment": "^2.29.4", + "pdf-lib": "^1.17.1", "pinia": "^2.0.29", "quasar": "^2.11.1", "socket.io-client": "^4.7.4", @@ -1143,6 +1145,24 @@ "node": ">= 8" } }, + "node_modules/@pdf-lib/standard-fonts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.6" + } + }, + "node_modules/@pdf-lib/upng": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.10" + } + }, "node_modules/@quasar/extras": { "version": "1.15.8", "license": "MIT", @@ -3006,6 +3026,12 @@ "node": ">=6.0.0" } }, + "node_modules/dom-to-image-more": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/dom-to-image-more/-/dom-to-image-more-3.6.0.tgz", + "integrity": "sha512-0BB0M9gRRP7znKBNLRAvNyWnkDIzSgMSDcS7WdPDzPnWhW2YJqxUR/dCHiJ2HdCV3K2rVky5Vba8UF31mvrCuQ==", + "license": "MIT" + }, "node_modules/domexception": { "version": "4.0.0", "dev": true, @@ -5871,6 +5897,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/panzoom": { "version": "9.4.3", "resolved": "https://registry.npmjs.org/panzoom/-/panzoom-9.4.3.tgz", @@ -5980,6 +6012,24 @@ "through": "~2.3" } }, + "node_modules/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "license": "MIT", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" + } + }, + "node_modules/pdf-lib/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/pdfjs-dist": { "version": "3.7.107", "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.7.107.tgz", diff --git a/package.json b/package.json index 68ab9bf..cdf5646 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,10 @@ "@tato30/vue-pdf": "^1.5.1", "@vuepic/vue-datepicker": "^3.6.3", "bma-org-chart": "^0.0.7", + "dom-to-image-more": "^3.6.0", "keycloak-js": "^20.0.2", "moment": "^2.29.4", + "pdf-lib": "^1.17.1", "pinia": "^2.0.29", "quasar": "^2.11.1", "socket.io-client": "^4.7.4", diff --git a/src/modules/12_organization/views/main.vue b/src/modules/12_organization/views/main.vue index 40a2cea..aa0a5f4 100644 --- a/src/modules/12_organization/views/main.vue +++ b/src/modules/12_organization/views/main.vue @@ -10,6 +10,12 @@ import { useCounterMixin } from "@/stores/mixin"; import "structure-chart/structure-chart.css"; +import { + exportChartToPNG, + exportChartToPDF, + showLoadingSpinner, +} from "@/plugins/exportChart"; + /** use*/ const $q = useQuasar(); const router = useRouter(); @@ -88,30 +94,33 @@ function findPath(id: any) { } } +const isLoadBtn = ref(false); /** function ดาวน์โหลดไฟล์โครงสร้าง PNG*/ async function savePNG() { - try { - showLoader(); - await scrollToCenter(); - await chartRef.value.savePNG(); - } catch { - messageError($q); - } finally { - hideLoader(); - } + showLoadingSpinner(); + isLoadBtn.value = true; + setTimeout(async () => { + try { + // export แบบเต็มๆ + scrollContainer.value && (await exportChartToPNG(scrollContainer.value)); + } finally { + isLoadBtn.value = false; + } + }, 500); } /** function ดาวน์โหลดไฟล์โครงสร้าง PDF*/ async function savePDF() { - try { - showLoader(); - await scrollToCenter(); - await chartRef.value.savePDF(); - } catch { - messageError($q); - } finally { - hideLoader(); - } + showLoadingSpinner(); + isLoadBtn.value = true; + setTimeout(async () => { + try { + // export แบบเต็มๆ + scrollContainer.value && (await exportChartToPDF(scrollContainer.value)); + } finally { + isLoadBtn.value = false; + } + }, 500); } /** ฟังก์ชันเลื่อน scroll ไปที่กึ่งกลาง*/ @@ -127,6 +136,7 @@ async function scrollToCenter() { * @param data */ async function refreshChart(data: any, type: number) { + if (isLoadBtn.value) return; // ถ้าโหลดอยู่ไม่ให้ทำอะไร if (data.value === undefined) { fetchStructChart(data, type.toString()); rootOrgID.value = data; @@ -229,8 +239,9 @@ onMounted(async () => {
@@ -239,7 +250,8 @@ onMounted(async () => { @@ -263,7 +275,9 @@ onMounted(async () => {
+
+
+ +
+ + `; + document.body.appendChild(loading); +} + +/** ฟังก์ชันสำหรับซ่อน loading spinner */ +function hideLoadingSpinner() { + const loading = document.getElementById("loading-spinner"); + if (loading) loading.remove(); +} + +/** + * ฟังก์ชันสำหรับ export โครงสร้างองค์กรเป็น PNG + * @param node HTMLElement ที่ต้องการ export เป็น PNG + */ +export async function exportChartToPNG(node: HTMLElement): Promise { + if (node) { + try { + // สร้าง PNG จาก DOM ขนาดเต็ม + const imageData = await domtoimage.toPng(node, { + bgcolor: "#fff", + quality: 1, + width: node.scrollWidth, + height: node.scrollHeight, + }); + + const link = document.createElement("a"); + link.download = "orgchart.png"; + link.href = imageData; + link.click(); + } catch (error: any) { + alert("Export ไม่สำเร็จ: " + error.message); + } finally { + hideLoadingSpinner(); + } + } +} + +/** + * ฟังก์ชันสำหรับ export โครงสร้างองค์กรเป็น PNG + * @param node HTMLElement ที่ต้องการ export เป็น PNG + */ +export async function exportChartToPDF(node: HTMLElement): Promise { + // ใช้ scrollWidth/scrollHeight เพื่อขนาดเต็ม + const width = node.scrollWidth; + const height = node.scrollHeight; + + try { + // สร้าง PNG จาก DOM ขนาดเต็ม + const imageData = await domtoimage.toPng(node, { + width, + height, + bgcolor: "#fff", + }); + // สร้าง PDF ด้วย pdf-lib + const pdfDoc = await PDFDocument.create(); + const page = pdfDoc.addPage([width, height]); + const pngImage = await pdfDoc.embedPng(imageData); + page.drawImage(pngImage, { + x: 0, + y: 0, + width, + height, + }); + const pdfBytes = await pdfDoc.save(); + // ดาวน์โหลด PDF + const blob = new Blob([pdfBytes], { type: "application/pdf" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "orgchart.pdf"; + a.click(); + URL.revokeObjectURL(url); + } catch (err: any) { + alert("Export ไม่สำเร็จ: " + err.message); + + console.error(err); + } finally { + hideLoadingSpinner(); + } +} diff --git a/src/types/dom-to-image-more.d.ts b/src/types/dom-to-image-more.d.ts new file mode 100644 index 0000000..3e07aba --- /dev/null +++ b/src/types/dom-to-image-more.d.ts @@ -0,0 +1,36 @@ +declare module "dom-to-image-more" { + export interface Options { + filter?: (node: Node) => boolean; + bgcolor?: string; + width?: number; + height?: number; + style?: any; + quality?: number; + imagePlaceholder?: string; + cacheBust?: boolean; + } + + export function toPng(node: HTMLElement, options?: Options): Promise; + export function toJpeg(node: HTMLElement, options?: Options): Promise; + export function toSvg(node: HTMLElement, options?: Options): Promise; + export function toPixelData( + node: HTMLElement, + options?: Options + ): Promise; + export function toCanvas( + node: HTMLElement, + options?: Options + ): Promise; + export function toBlob(node: HTMLElement, options?: Options): Promise; + + const domtoimage: { + toPng: typeof toPng; + toJpeg: typeof toJpeg; + toSvg: typeof toSvg; + toPixelData: typeof toPixelData; + toCanvas: typeof toCanvas; + toBlob: typeof toBlob; + }; + + export default domtoimage; +}