refactor file tree

This commit is contained in:
Timi
2026-08-11 15:24:36 +08:00
parent 575d41b2e2
commit 819c1623eb
42 changed files with 86 additions and 86 deletions
+278
View File
@@ -0,0 +1,278 @@
import { BarcodeFormat, EncodeHintType, QRCodeWriter } from "@zxing/library";
export interface QrCodePage {
pageIndex: number;
codes: string[];
}
export interface DownloadQrCodeOptions {
codes: string[];
filePrefix: string;
maxCodeLength?: number;
}
export interface PrintQrCodeOptions {
codes: string[];
maxCodeLength?: number;
title?: string;
}
export interface QrCodeGrid {
rows: number;
columns: number;
pageSize: number;
}
const pageWidth = 2480;
const pageHeight = 3508;
const pagePadding = 80;
const qrSize = 300;
const minCellGap = 18;
const codeFontSize = 34;
const codeTopGap = 3;
const gridLineWidth = 2;
const codeTextHorizontalPadding = 28;
const codeTextWidthRatio = .62;
const qrWriter = new QRCodeWriter();
const qrEncodeHints = new Map<EncodeHintType, string | number>([
[EncodeHintType.ERROR_CORRECTION, "H"],
[EncodeHintType.CHARACTER_SET, "UTF-8"],
[EncodeHintType.MARGIN, 4]
]);
export function calculateQrCodeGrid(maxCodeLength = 0): QrCodeGrid {
const codeTextMinWidth = Math.ceil(maxCodeLength * codeFontSize * codeTextWidthRatio + codeTextHorizontalPadding);
const cellMinWidth = Math.max(qrSize + minCellGap, codeTextMinWidth + minCellGap);
const cellMinHeight = qrSize + codeTopGap + codeFontSize + minCellGap;
const columns = Math.max(1, Math.floor((pageWidth - pagePadding * 2) / cellMinWidth));
const rows = Math.max(1, Math.floor((pageHeight - pagePadding * 2) / cellMinHeight));
return {
rows,
columns,
pageSize: rows * columns
};
}
export function splitQrCodePages(codes: string[], maxCodeLength = 0): QrCodePage[] {
const pages: QrCodePage[] = [];
const pageSize = calculateQrCodeGrid(maxCodeLength).pageSize;
for (let index = 0; index < codes.length; index += pageSize) {
pages.push({
pageIndex: pages.length,
codes: codes.slice(index, index + pageSize)
});
}
return pages;
}
export async function downloadQrCodeImages(options: DownloadQrCodeOptions): Promise<void> {
const pages = splitQrCodePages(options.codes, options.maxCodeLength);
for (const page of pages) {
const canvas = renderQrCodePage(page.codes, options.maxCodeLength);
await downloadCanvas(canvas, `${options.filePrefix}-${String(page.pageIndex + 1).padStart(2, "0")}.png`);
}
}
export async function printQrCodeImages(options: PrintQrCodeOptions): Promise<void> {
const printWindow = window.open("", "_blank");
if (!printWindow) {
throw new Error("浏览器阻止了打印窗口,请允许弹出窗口后重试");
}
const pages = splitQrCodePages(options.codes, options.maxCodeLength);
const imageList = pages.map(page => renderQrCodePage(page.codes, options.maxCodeLength).toDataURL("image/png"));
const title = options.title || "客户二维码打印";
const imageHTML = imageList.map(src => `<section class="page"><img src="${src}" alt="客户二维码" /></section>`).join("");
printWindow.document.open();
printWindow.document.write(`<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>${escapeHTML(title)}</title>
<style>
@page {
size: A4 portrait;
margin: 0;
}
html,
body {
margin: 0;
padding: 0;
background: #fff;
}
.page {
margin: 0;
padding: 0;
width: 210mm;
height: 297mm;
display: flex;
overflow: hidden;
page-break-after: always;
align-items: center;
justify-content: center;
}
.page:last-child {
page-break-after: auto;
}
img {
width: 210mm;
height: 297mm;
display: block;
}
</style>
</head>
<body>${imageHTML}</body>
</html>`);
printWindow.document.close();
await waitPrintImagesLoaded(printWindow.document);
printWindow.focus();
printWindow.print();
}
function renderQrCodePage(codes: string[], maxCodeLength = 0): HTMLCanvasElement {
const canvas = document.createElement("canvas");
canvas.width = pageWidth;
canvas.height = pageHeight;
const grid = calculateQrCodeGrid(maxCodeLength);
const context = canvas.getContext("2d");
if (!context) {
throw new Error("当前浏览器不支持 Canvas");
}
context.fillStyle = "#ffffff";
context.fillRect(0, 0, pageWidth, pageHeight);
context.fillStyle = "#000000";
context.font = `500 ${codeFontSize}px Arial, sans-serif`;
context.textAlign = "center";
context.textBaseline = "top";
const cellWidth = (pageWidth - pagePadding * 2) / grid.columns;
const cellHeight = (pageHeight - pagePadding * 2) / grid.rows;
drawGridLines(context, grid, cellWidth, cellHeight);
codes.forEach((code, index) => {
const rowIndex = Math.floor(index / grid.columns);
const columnIndex = index % grid.columns;
const cellLeft = pagePadding + columnIndex * cellWidth;
const cellTop = pagePadding + rowIndex * cellHeight;
const qrLeft = Math.round(cellLeft + (cellWidth - qrSize) / 2);
const qrTop = Math.round(cellTop + (cellHeight - qrSize - codeTopGap - codeFontSize) / 2);
drawQrCode(context, code, qrLeft, qrTop, qrSize);
context.fillStyle = "#000000";
context.fillText(code, cellLeft + cellWidth / 2, qrTop + qrSize + codeTopGap);
});
return canvas;
}
async function waitPrintImagesLoaded(document: Document): Promise<void> {
const imageList = Array.from(document.images);
await Promise.all(imageList.map(image => {
if (image.complete) {
return Promise.resolve();
}
return new Promise<void>((resolve, reject) => {
image.onload = () => resolve();
image.onerror = () => reject(new Error("打印图片加载失败"));
});
}));
}
function escapeHTML(value: string): string {
return value
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll("\"", "&quot;");
}
function drawGridLines(context: CanvasRenderingContext2D, grid: QrCodeGrid, cellWidth: number, cellHeight: number): void {
context.strokeStyle = "#d1d5db";
context.lineWidth = gridLineWidth;
for (let rowIndex = 0; rowIndex <= grid.rows; rowIndex++) {
const top = Math.round(pagePadding + rowIndex * cellHeight);
context.beginPath();
context.moveTo(pagePadding, top);
context.lineTo(pageWidth - pagePadding, top);
context.stroke();
}
for (let columnIndex = 0; columnIndex <= grid.columns; columnIndex++) {
const left = Math.round(pagePadding + columnIndex * cellWidth);
context.beginPath();
context.moveTo(left, pagePadding);
context.lineTo(left, pageHeight - pagePadding);
context.stroke();
}
}
function drawQrCode(context: CanvasRenderingContext2D, code: string, left: number, top: number, qrSize: number): void {
const matrix = qrWriter.encode(code, BarcodeFormat.QR_CODE, qrSize, qrSize, qrEncodeHints);
const width = matrix.getWidth();
const height = matrix.getHeight();
const sourceCanvas = document.createElement("canvas");
sourceCanvas.width = width;
sourceCanvas.height = height;
const sourceContext = sourceCanvas.getContext("2d");
if (!sourceContext) {
throw new Error("当前浏览器不支持 Canvas");
}
sourceContext.fillStyle = "#ffffff";
sourceContext.fillRect(0, 0, width, height);
sourceContext.fillStyle = "#000000";
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (matrix.get(x, y)) {
sourceContext.fillRect(x, y, 1, 1);
}
}
}
context.fillStyle = "#ffffff";
context.fillRect(left, top, qrSize, qrSize);
context.imageSmoothingEnabled = false;
context.drawImage(sourceCanvas, left, top, qrSize, qrSize);
context.imageSmoothingEnabled = true;
}
async function downloadCanvas(canvas: HTMLCanvasElement, fileName: string): Promise<void> {
const blob = await new Promise<Blob>((resolve, reject) => {
canvas.toBlob(result => {
if (result) {
resolve(result);
return;
}
reject(new Error("图片生成失败"));
}, "image/png");
});
const link = document.createElement("a");
const url = URL.createObjectURL(blob);
link.href = url;
link.download = fileName;
link.click();
await new Promise<void>(resolve => {
window.setTimeout(resolve, 80);
});
URL.revokeObjectURL(url);
}