lodop.ts 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. import { qrPngDataUrl } from "./qrImage";
  2. export const CLODOP_SETUP_URL =
  3. (import.meta.env.BASE_URL || "/") + "lodop/CLodop_Setup_for_Win32NT.exe";
  4. const CLODOP_JS_URLS = [
  5. "http://localhost:8000/CLodopfuncs.js",
  6. "http://localhost:18000/CLodopfuncs.js"
  7. ];
  8. export class LodopMissingError extends Error {
  9. constructor(message = "未检测到 CLodop 打印服务,请先安装并启动打印脚本") {
  10. super(message);
  11. this.name = "LodopMissingError";
  12. }
  13. }
  14. function winLodop() {
  15. const win = window as any;
  16. try {
  17. if (typeof win.getCLodop === "function") {
  18. return win.getCLodop() || null;
  19. }
  20. } catch {
  21. /* 服务未就绪 */
  22. }
  23. return win.CLODOP || null;
  24. }
  25. function loadScript(src: string, timeout = 4000) {
  26. return new Promise<void>((resolve, reject) => {
  27. const existed = document.querySelector(
  28. `script[data-clodop="1"][src="${src}"]`
  29. ) as HTMLScriptElement | null;
  30. if (existed) {
  31. if (winLodop()) {
  32. resolve();
  33. return;
  34. }
  35. }
  36. const script = document.createElement("script");
  37. script.src = src;
  38. script.setAttribute("data-clodop", "1");
  39. const timer = window.setTimeout(() => {
  40. script.onload = null;
  41. script.onerror = null;
  42. reject(new Error("timeout:" + src));
  43. }, timeout);
  44. script.onload = () => {
  45. window.clearTimeout(timer);
  46. resolve();
  47. };
  48. script.onerror = () => {
  49. window.clearTimeout(timer);
  50. script.remove();
  51. reject(new Error("fail:" + src));
  52. };
  53. document.head.appendChild(script);
  54. });
  55. }
  56. export function isCLodopReady() {
  57. return !!winLodop();
  58. }
  59. export async function loadLodop(jsUrl?: string) {
  60. const existing = winLodop();
  61. if (existing) {
  62. return existing;
  63. }
  64. const urls = jsUrl
  65. ? [jsUrl, ...CLODOP_JS_URLS.filter(url => url !== jsUrl)]
  66. : CLODOP_JS_URLS.slice();
  67. for (const url of urls) {
  68. try {
  69. await loadScript(url);
  70. const lodop = winLodop();
  71. if (lodop) {
  72. return lodop;
  73. }
  74. } catch {
  75. /* 换下一个端口 */
  76. }
  77. }
  78. throw new LodopMissingError();
  79. }
  80. export async function probeCLodop(jsUrl?: string) {
  81. try {
  82. await loadLodop(jsUrl);
  83. return true;
  84. } catch {
  85. return false;
  86. }
  87. }
  88. export function downloadCLodop() {
  89. const a = document.createElement("a");
  90. a.href = CLODOP_SETUP_URL;
  91. a.download = "CLodop_Setup_for_Win32NT.exe";
  92. document.body.appendChild(a);
  93. a.click();
  94. a.remove();
  95. }
  96. export function tryStartCLodop() {
  97. window.location.href = "CLodop.protocol:setup";
  98. }
  99. export async function runLodop(lodopPack: any) {
  100. const LODOP = await loadLodop(lodopPack && lodopPack.js_url);
  101. const title = (lodopPack && lodopPack.title) || "流程单";
  102. const copies = Math.max(1, Number(lodopPack && lodopPack.copies) || 1);
  103. const page = (lodopPack && lodopPack.page) || {};
  104. const w = Number(page.w) || 210;
  105. const h = Number(page.h) || 297;
  106. const pages = Array.isArray(lodopPack && lodopPack.pages) ? lodopPack.pages : [];
  107. LODOP.PRINT_INIT(title);
  108. pickCapablePrinter(LODOP);
  109. const paperName = paperNameOf(w, h, page.paper_size);
  110. if (paperName) {
  111. LODOP.SET_PRINT_PAGESIZE(1, 0, 0, paperName);
  112. } else {
  113. LODOP.SET_PRINT_PAGESIZE(1, Math.round(w * 10), Math.round(h * 10), "");
  114. }
  115. LODOP.SET_PRINT_MODE("POS_BASEON_PAPER", 1);
  116. LODOP.SET_PRINT_COPIES(copies);
  117. applyLodopBaseTypeface(LODOP);
  118. if (!pages.length) {
  119. throw new Error("没有可套打的页面数据");
  120. }
  121. const textsPages = Array.isArray(lodopPack && lodopPack.print_texts)
  122. ? lodopPack.print_texts
  123. : pages.map((sheet: any) => sheet.print_items || sheet.items || []);
  124. for (let index = 0; index < pages.length; index++) {
  125. const sheet = pages[index];
  126. const items = Array.isArray(textsPages[index]) ? textsPages[index] : [];
  127. let added = 0;
  128. for (const row of items) {
  129. const text = String((row && row.text) || "");
  130. if (!text) {
  131. continue;
  132. }
  133. if (row.type === "qrcode") {
  134. await addLodopQrImage(LODOP, row, text);
  135. added += 1;
  136. continue;
  137. }
  138. LODOP.ADD_PRINT_TEXT(
  139. row.top || "0%",
  140. row.left || "0%",
  141. row.width || "10%",
  142. row.height || "4%",
  143. text
  144. );
  145. applyLodopTextStyle(LODOP, row);
  146. added += 1;
  147. }
  148. if (!added && sheet && sheet.html) {
  149. LODOP.ADD_PRINT_HTM(0, 0, "100%", "100%", sheet.html);
  150. added += 1;
  151. }
  152. if (!added) {
  153. LODOP.ADD_PRINT_TEXT("8%", "8%", "80%", "10%", "套打数据为空");
  154. applyLodopTextStyle(LODOP, { font: "微软雅黑", size: 10, bold: 0, align: 1 });
  155. }
  156. if (index < pages.length - 1) {
  157. LODOP.NEWPAGE();
  158. }
  159. }
  160. if ((lodopPack && lodopPack.print_api) === "PRINT") {
  161. LODOP.PRINT();
  162. } else {
  163. LODOP.PREVIEW();
  164. }
  165. }
  166. async function addLodopQrImage(lodop: any, row: any, text: string) {
  167. const top = row.top || "0mm";
  168. const left = row.left || "0mm";
  169. const width = row.width || "24mm";
  170. const height = row.height || "24mm";
  171. try {
  172. const png = await qrPngDataUrl(text, 640);
  173. lodop.ADD_PRINT_IMAGE(
  174. top,
  175. left,
  176. width,
  177. height,
  178. `<img border="0" src="${png}" />`
  179. );
  180. lodop.SET_PRINT_STYLEA(0, "Stretch", 1);
  181. } catch {
  182. lodop.ADD_PRINT_BARCODE(top, left, width, height, "QRCode", text);
  183. try {
  184. lodop.SET_PRINT_STYLEA(0, "QRCodeErrorLevel", "L");
  185. lodop.SET_PRINT_STYLEA(0, "QRCodeVersion", 4);
  186. lodop.SET_PRINT_STYLEA(0, "DataCharset", "UTF-8");
  187. } catch {
  188. /* 旧版 CLodop 无此样式 */
  189. }
  190. }
  191. }
  192. function applyLodopBaseTypeface(lodop: any) {
  193. try {
  194. lodop.SET_PRINT_STYLE("FontName", "微软雅黑");
  195. lodop.SET_PRINT_STYLE("FontSize", 10);
  196. lodop.SET_PRINT_STYLE("Bold", 0);
  197. lodop.SET_PRINT_STYLE("Italic", 0);
  198. } catch {
  199. /* 旧版 CLodop 无全局样式 */
  200. }
  201. }
  202. function applyLodopTextStyle(lodop: any, row: any) {
  203. lodop.SET_PRINT_STYLEA(0, "FontName", (row && row.font) || "微软雅黑");
  204. lodop.SET_PRINT_STYLEA(0, "FontSize", Number(row && row.size) || 10);
  205. lodop.SET_PRINT_STYLEA(0, "Bold", row && row.bold ? 1 : 0);
  206. lodop.SET_PRINT_STYLEA(0, "Alignment", Number(row && row.align) || 1);
  207. try {
  208. lodop.SET_PRINT_STYLEA(0, "Italic", 0);
  209. lodop.SET_PRINT_STYLEA(0, "Underline", 0);
  210. } catch {
  211. /* 旧版 CLodop 无斜体/下划线样式 */
  212. }
  213. }
  214. function paperNameOf(w: number, h: number, name?: string) {
  215. const n = String(name || "").toUpperCase();
  216. if (n === "A5" || (Math.abs(w - 148) < 1 && Math.abs(h - 210) < 1)) {
  217. return "A5";
  218. }
  219. if (n === "A4" || (Math.abs(w - 210) < 1 && Math.abs(h - 297) < 1)) {
  220. return "A4";
  221. }
  222. return "";
  223. }
  224. function pickCapablePrinter(lodop: any) {
  225. let count = 0;
  226. try {
  227. count = Number(lodop.GET_PRINTER_COUNT()) || 0;
  228. } catch {
  229. return;
  230. }
  231. const prefer = [
  232. "Microsoft Print to PDF",
  233. "导出为WPS PDF",
  234. "Microsoft XPS Document Writer",
  235. "WPS PDF"
  236. ];
  237. const names: string[] = [];
  238. for (let i = 0; i < count; i++) {
  239. names.push(String(lodop.GET_PRINTER_NAME(i) || ""));
  240. }
  241. const hit =
  242. prefer.find(name => names.includes(name)) ||
  243. names.find(name => /pdf|xps/i.test(name));
  244. if (!hit) {
  245. return;
  246. }
  247. try {
  248. lodop.SET_PRINTER_INDEXA(hit);
  249. } catch {
  250. /* 预览里仍可手选打印机 */
  251. }
  252. }
  253. export async function handleLodopError(err: unknown) {
  254. const { ElMessage, ElMessageBox } = await import("element-plus");
  255. const message =
  256. err instanceof Error ? err.message : "Lodop 预览未打开,请确认本机已启动 CLodop";
  257. try {
  258. await ElMessageBox.confirm(
  259. message + "。需要先在本机安装并启动 CLodop 打印服务,安装后请保持托盘图标运行并刷新本页。",
  260. "安装打印脚本",
  261. {
  262. confirmButtonText: "下载安装程序",
  263. cancelButtonText: "已安装,尝试启动",
  264. distinguishCancelAndClose: true,
  265. type: "warning"
  266. }
  267. );
  268. downloadCLodop();
  269. ElMessage.info("正在下载 CLodop_Setup_for_Win32NT.exe,请运行安装程序");
  270. } catch (action) {
  271. if (action === "cancel") {
  272. tryStartCLodop();
  273. ElMessage.info("已尝试启动本机 CLodop,请稍候再点套打");
  274. } else {
  275. ElMessage.warning(message);
  276. }
  277. }
  278. }