This commit is contained in:
Viet An
2026-06-25 17:25:42 +07:00
parent 31ac60a282
commit f759ca49d5
13 changed files with 669 additions and 1458 deletions

View File

@@ -282,7 +282,7 @@
</button>
<button
class="button is-light"
@click="$exportpdf(docid, record.code)"
@click="exportPdf(docid, { filename: record.code })"
>
In thông tin
</button>

View File

@@ -0,0 +1,30 @@
<script setup>
import { Html5Qrcode } from "html5-qrcode";
const emit = defineEmits(["scanned"]);
let scanner;
onMounted(async () => {
scanner = new Html5Qrcode("scanner-region");
await scanner.start(
{ facingMode: "environment" }, // or 'user' for front cam
{ fps: 10, qrbox: 450 },
(decodedText) => {
console.log("scanned", decodedText);
emit("scanned", decodedText);
scanner.stop(); // stop after first scan
},
(errorMsg) => {
console.log("errorMsg", errorMsg);
}, // ignore per-frame scan failures
);
});
onBeforeUnmount(() => {
scanner?.stop().catch(() => {});
});
</script>
<template>
<div id="scanner-region"></div>
</template>

View File

@@ -1,8 +1,16 @@
<script setup>
import BarcodeScanner from "~/components/imports/BarcodeScanner.vue";
import Products from "~/components/imports/Products.vue";
function onScanned(code) {
console.log("code", code);
}
</script>
<template>
<div class="fixed-grid has-12-cols">
<!-- <ClientOnly>
<BarcodeScanner @scanned="onScanned" />
</ClientOnly> -->
<div class="grid">
<div class="cell is-col-span-12">
<Products />

View File

@@ -164,7 +164,7 @@
</button>
<button
class="button is-light has-text-black"
@click="$exportpdf(docid, record.code)"
@click="exportPdf(docid, { filename: record.code })"
>
{{ findLang("print") }}
</button>

View File

@@ -232,61 +232,6 @@ export default defineNuxtPlugin((nuxtApp) => {
return id;
};
const exportpdf = function (docid, name, size, orientation = "portrait") {
var element = document.getElementById(docid);
let elms = element.querySelectorAll("[id='ignore']");
for (var i = 0; i < elms.length; i++) {
elms[i].style.setProperty("display", "none", "important");
}
// Xóa cache ảnh để lần export sau không bị lỗi
element.querySelectorAll("img").forEach((img) => {
img.src = `${img.src}`;
});
var opt = {
margin: 0.4,
filename: `${name || "file"}-${dayjs().format("YYYYMMDDHHmmss")}.pdf`,
image: { type: "jpeg", quality: 1 },
html2canvas: { scale: 3, useCORS: true },
jsPDF: { unit: "in", format: size || "a4", orientation },
pagebreak: {
mode: ["avoid-all", "css", "legacy"],
before: ".page-break-before",
after: ".page-break-after",
avoid: ".avoid-page-break",
},
};
setTimeout(() => html2pdf().set(opt).from(element).save(), 300);
setTimeout(() => {
for (var i = 0; i < elms.length; i++) {
elms[i].style.removeProperty("display");
}
}, 1000);
return opt.filename;
};
const exportImage = function (docid, name) {
var element = document.getElementById(docid);
let elms = element.querySelectorAll("[id='ignore']");
for (var i = 0; i < elms.length; i++) {
elms[i].style.display = "none";
}
let filename = `${name}-${dayjs().format("YYYYMMDDHHmmss")}.png`;
html2canvas(element).then((canvas) => {
const link = document.createElement("a");
link.href = canvas.toDataURL("image/png");
link.download = filename;
link.click();
link.remove();
});
setTimeout(() => {
for (var i = 0; i < elms.length; i++) {
elms[i].style.display = "block";
}
}, 1000);
return filename;
};
const download = async function (url, fileName) {
const name = dayjs(new Date()).format("YYYYMMDDhhmmss") + "-" + fileName;
const response = await fetch(url);
@@ -549,8 +494,6 @@ export default defineNuxtPlugin((nuxtApp) => {
copyToClipboard,
nonAccent,
linkID,
exportpdf,
exportImage,
vnmoney,
createMeta,
dayjs,

28
app/utils/exportImage.js Normal file
View File

@@ -0,0 +1,28 @@
import dayjs from "dayjs";
import html2canvas from "html2canvas-pro";
export default async function exportImage(docid, filename = "file") {
const target = document.getElementById(docid);
const ignored = target.querySelectorAll(".ignore");
ignored.forEach((el) => el.style.setProperty("display", "none", "important"));
// Xóa cache ảnh để lần export sau không bị lỗi
target.querySelectorAll("img").forEach((img) => (img.src = `${img.src}`));
const restoreIcons = swapIconsForExport(target);
try {
await new Promise((r) => requestAnimationFrame(r));
const canvas = await html2canvas(target);
const link = document.createElement("a");
link.href = canvas.toDataURL("image/png");
link.download = `${filename}-${dayjs().format("YYYYMMDDHHmmss")}.png`;
link.click();
link.remove();
} finally {
restoreIcons();
ignored.forEach((el) => el.style.removeProperty("display"));
}
return filename;
}

35
app/utils/exportPdf.js Normal file
View File

@@ -0,0 +1,35 @@
import dayjs from "dayjs";
import html2pdf from "html2pdf.js";
export default async function exportPdf(docid, { filename = "file", format = "a4", orientation = "portrait" } = {}) {
const target = document.getElementById(docid);
const ignored = target.querySelectorAll(".ignore");
ignored.forEach((el) => el.style.setProperty("display", "none", "important"));
// Xóa cache ảnh để lần export sau không bị lỗi
target.querySelectorAll("img").forEach((img) => (img.src = `${img.src}`));
const restoreIcons = swapIconsForExport(target);
const opt = {
margin: 3,
filename: `${filename}-${dayjs().format("YYYYMMDDHHmmss")}.pdf`,
jsPDF: { format, orientation, unit: "mm" },
html2canvas: { scale: 3, useCORS: true },
image: { type: "jpeg", quality: 1 },
pagebreak: {
mode: ["avoid-all", "css", "legacy"],
before: ".page-break-before",
after: ".page-break-after",
avoid: ".avoid-page-break",
},
};
try {
await new Promise((r) => requestAnimationFrame(r));
await html2pdf().set(opt).from(target).save();
} finally {
restoreIcons();
ignored.forEach((el) => el.style.removeProperty("display"));
}
return opt.filename;
}

52
app/utils/iconSwap.js Normal file
View File

@@ -0,0 +1,52 @@
const ICONIFY_SELECTOR = '[class*="iconify"]';
/**
* Replaces iconify CSS-mask spans with real <svg> (for html2pdf, html2canvas),
* returns a restore function to swap them back.
*/
export function swapIconsForExport(root) {
const spans = root.querySelectorAll(ICONIFY_SELECTOR);
const restoreList = [];
spans.forEach((span) => {
const svgMarkup = extractSvgFromMask(span);
if (!svgMarkup) return; // not a mask-based icon, skip
const computed = getComputedStyle(span);
const size = computed.fontSize || "1em";
const color = computed.color;
const wrapper = document.createElement("span");
wrapper.innerHTML = svgMarkup;
const svgEl = wrapper.firstElementChild;
svgEl.setAttribute("width", size);
svgEl.setAttribute("height", size);
svgEl.style.display = "inline-block";
svgEl.style.verticalAlign = "middle";
// force fill to currentColor's resolved value
svgEl.querySelectorAll("[fill]").forEach((node) => node.setAttribute("fill", color));
span.replaceWith(svgEl);
restoreList.push({ svgEl, originalSpan: span });
});
return function restoreIcons() {
restoreList.forEach(({ svgEl, originalSpan }) => {
svgEl.replaceWith(originalSpan);
});
};
}
function extractSvgFromMask(el) {
const maskVar = getComputedStyle(el).getPropertyValue("--svg").trim();
// maskVar looks like: url("data:image/svg+xml,...")
const match = maskVar.match(/url\((['"]?)(.*?)\1\)/);
if (!match) return null;
const dataUri = match[2];
if (!dataUri.startsWith("data:image/svg+xml,")) return null;
const encoded = dataUri.replace("data:image/svg+xml,", "");
return decodeURIComponent(encoded);
}