Ver código fonte

对接佳博打印机 SDK

cc12458 2 semanas atrás
pai
commit
836a2a0c0d

+ 17 - 1
@types/bridge.d.ts

@@ -6,7 +6,15 @@ interface ScanData {
 }
 
 export interface BridgeEventMap {
-  scan: CustomEvent<{code: number, data?: ScanData, message?: string}>;
+  scan: CustomEvent<{ code: number; data?: ScanData; message?: string }>;
+  ['print:connect']: CustomEvent<{ code: number; data?: BridgePrinterDevice | true; message?: string }>;
+  ['print:disconnect']: CustomEvent<{ code: number; data?: BridgePrinterDevice | true; message?: string }>;
+}
+
+export interface BridgePrinterDevice {
+  type: 'wifi';
+  ip?: string;
+  port?: number;
 }
 
 export class Bridge extends EventTarget {
@@ -14,6 +22,7 @@ export class Bridge extends EventTarget {
 
   public static print(): Promise<void>;
   public static print(params: { url?: string }): Promise<void>;
+  public static print(params: { tspl: String; device?: BridgePrinterDevice }): Promise<void>;
 
   public static scan(params?: { timeout?: number; signal?: AbortSignal }): Promise<ScanData>;
 
@@ -25,4 +34,11 @@ export class Bridge extends EventTarget {
    */
   addEventListener<T extends keyof BridgeEventMap>(type: T, listener: (event: BridgeEventMap[T]) => void, options?: boolean | AddEventListenerOptions): () => void;
   removeEventListener<T extends keyof BridgeEventMap>(type: T, listener: (event: BridgeEventMap[T]) => void, options?: boolean | AddEventListenerOptions): () => void;
+
+  postMessage(...args: any[]): Promise<void>;
+
+  public printer: {
+    connect(device: BridgePrinterDevice): Promise<[device: BridgePrinterDevice, onCleanup: () => Promise<BridgePrinterDevice>]>;
+    disconnect(device: BridgePrinterDevice): Promise<BridgePrinterDevice>;
+  };
 }

+ 1 - 0
@types/global.d.ts

@@ -14,6 +14,7 @@ declare global {
   }
 
   type BridgeEventMap = import('./bridge').BridgeEventMap;
+  type BridgePrinterDevice = import('./bridge').BridgePrinterDevice;
 
   /**
    * webview 设备注入的 全局对象(历史遗留)

+ 12 - 0
src/core/launch/platform.launch.ts

@@ -23,6 +23,18 @@ export default function bridgeLoader(): Launcher {
   return async function () {
     if (platformIsPDA()) {
       await waitFor(() => window.bridge != null);
+      window.bridge.printer = {
+        connect(device: BridgePrinterDevice) {
+          const { promise, ...resolvers } = Promise.withResolvers<BridgePrinterDevice>();
+          window.bridge.postMessage('print:connect', device, resolvers);
+          return promise.then((device) => [device, () => this.disconnect(device)]);
+        },
+        disconnect(device: BridgePrinterDevice) {
+          const { promise, ...resolvers } = Promise.withResolvers<BridgePrinterDevice>();
+          window.bridge.postMessage('print:disconnect', device, resolvers);
+          return promise;
+        },
+      };
     }
   };
 }

+ 191 - 0
src/module/print/Printer.vue

@@ -0,0 +1,191 @@
+<script setup lang="ts">
+import { type FormInstance, showToast } from 'vant';
+import { tryOnMounted, tryOnUnmounted } from '@vueuse/core';
+import { platformIsPDA } from '@/platform';
+import { usePrintStore } from '@/stores';
+import { useSnapshot } from '@/module/print/useSnapshot.ts';
+import { regex } from '@/tools/validator.ts';
+import { browserPrint } from '@/stores/print.store.ts';
+
+const props = defineProps<{
+  disabled?: boolean;
+  loader: () => Promise<{ default: Component }>;
+  loaderParams: Record<string, any>;
+  onPrintBefore?: () => boolean | Promise<boolean>;
+  onPrintAfter?: () => void | Promise<void>;
+}>();
+const emits = defineEmits<{ start: [] }>();
+
+const Printer = usePrintStore();
+
+watch(
+  () => props.loaderParams,
+  (value, oldValue) => {
+    if (JSON.stringify(value) !== JSON.stringify(oldValue)) snapshot.value = '';
+  },
+  { immediate: false },
+);
+
+const printing = defineModel<boolean>('loading', { default: false });
+const total = defineModel('total', { default: 1 });
+const host = defineModel('host', { default: '' });
+
+const show = ref(false);
+const connecting = ref(false);
+
+const rules = reactive({
+  host: [
+    { required: true, message: '请输入打印机 IP 地址' },
+    {
+      validator: (value: string) => regex.host.test(value),
+      message: `IP 地址格式不正确`,
+    },
+  ],
+  total: [{ required: true, message: '请输入打印份数' }],
+});
+
+const { snapshot, tspl, render } = useSnapshot(props.loader);
+
+const open = async () => {
+  if ((await props.onPrintBefore?.()) ?? true) {
+    printing.value = true;
+    try {
+      const { snapshot: src } = await render(props.loaderParams);
+      if (platformIsPDA()) show.value = true;
+      else browserPrint(src);
+    } catch (error: any) {
+      showNotify({ message: `创建打印数据错误(${error?.message})`, type: 'danger' });
+    }
+    printing.value = false;
+  }
+};
+
+const form = useTemplateRef<FormInstance>('printer-form');
+const validate = (name?: string) =>
+  form.value?.validate(name).then(
+    () => true,
+    () => false,
+  );
+
+const start = (action: 'cancel' | 'confirm') => {
+  if (action === 'confirm') return validate()?.then((valid) => valid && print()) ?? false;
+  if (action === 'cancel') {
+    printing.value = false;
+    form.value?.resetValidation?.()
+  }
+  return true;
+};
+
+async function print() {
+  if (platformIsPDA()) {
+    if (connecting.value || !host.value) return false;
+
+    try {
+      const data = tspl.value?.(total.value);
+      await Printer.print(host.value, data!!);
+      showToast({ message: `开始打印`, type: 'success' });
+    } catch (error: any) {
+      showNotify({ message: `创建打印任务错误(${error?.message})`, type: 'danger' });
+    }
+  }
+  return true;
+}
+
+const connected = ref(false);
+
+async function toggle() {
+  const valid = await validate('host');
+  if (!valid) return;
+
+  connecting.value = true;
+  try {
+    const device = Printer.getDevice(host.value);
+    if (connected.value) {
+      await Printer.disconnect(device);
+      showToast({ message: '断开成功', type: 'success' });
+      connected.value = false;
+    } else {
+      await Printer.connect(device);
+      connected.value = true;
+      showToast({ message: '连接成功', type: 'success' });
+    }
+  } catch (error: any) {
+    showNotify({ message: `操作失败(${error?.message})`, type: 'warning' });
+    connected.value = false;
+  }
+
+  connecting.value = false;
+}
+
+let connectListener: Array<() => void> = [];
+tryOnMounted(() => {
+  if (!host.value) host.value = Printer.host || '';
+  if (!connected.value) toggle();
+  if (platformIsPDA()) {
+    connectListener.push(
+      window.bridge.addEventListener('print:connect', () => {
+        connected.value = true;
+      }),
+      window.bridge.addEventListener('print:disconnect', () => {
+        connected.value = false;
+      }),
+    );
+  }
+});
+tryOnUnmounted(() => {
+  connectListener.forEach((fn) => fn?.());
+  connectListener = [];
+  snapshot.value = '';
+});
+</script>
+
+<template>
+  <van-dialog v-model:show="show" title="连接打印机" :confirm-button-disabled="connecting" confirm-button-text="打印" show-cancel-button :before-close="start">
+    <van-form ref="printer-form" @submit="print">
+      <van-cell-group>
+        <van-field
+          name="host"
+          label="打印设置"
+          placeholder="打印机 IP 地址"
+          v-model="host"
+          :rules="rules['host']"
+          :readonly="connecting"
+          :disabled="connected"
+          enterkeyhint="go"
+          @keydown.enter="toggle()"
+        >
+          <template #button>
+            <van-button size="small" :type="connected ? 'danger' : 'primary'" :loading="connecting" @click="toggle">
+              {{ connected ? '断开' : '连接' }}
+            </van-button>
+          </template>
+        </van-field>
+        <van-field class="suffix" v-model="total" name="total" :rules="rules['total']" type="digit" label="打印包数" placeholder="请输入" :min="1">
+          <template #extra>
+            <div v-if="total">包</div>
+          </template>
+        </van-field>
+        <van-cell title="温馨提示" label="输入几包就打印几张标签" />
+      </van-cell-group>
+    </van-form>
+  </van-dialog>
+  <van-button block :loading="printing" :disabled="props.disabled" @click="open()">打印标签</van-button>
+</template>
+
+<style scoped lang="scss">
+.van-field.suffix {
+  :deep(.van-field__value) {
+    flex: none;
+    //width: min-content;
+    //min-width: 120px;
+    max-width: 170px;
+
+    input {
+      text-align: center;
+    }
+  }
+}
+:deep(.van-field__label) {
+  align-self: center;
+}
+</style>

+ 1 - 0
src/stores/index.ts

@@ -13,3 +13,4 @@ pinia.use(persistedState);
 export default pinia;
 export { useAccountStore } from './account.store.ts';
 export { useStepStore } from './step.store.ts';
+export { usePrintStore } from './print.store.ts';

+ 112 - 0
src/stores/print.store.ts

@@ -0,0 +1,112 @@
+import { defineStore } from 'pinia';
+import { regex } from '@/tools/validator.ts';
+
+const DEFAULT_PORT = 9100;
+
+export const usePrintStore = defineStore(
+  'print',
+  () => {
+    const ip = ref('');
+    const port = ref(DEFAULT_PORT);
+
+    const host = computed(() => {
+      const _ip = ip.value;
+      const _port = port.value || DEFAULT_PORT;
+      return _ip ? `${_ip}:${_port}` : void 0;
+    });
+
+    const _device = computed(() => {
+      const _ip = ip.value;
+      const _port = port.value || DEFAULT_PORT;
+      return _ip ? ({ type: 'wifi', ip: _ip, port: _port } as BridgePrinterDevice) : void 0;
+    });
+
+    function $reset() {
+      ip.value = '';
+      port.value = DEFAULT_PORT;
+    }
+
+    function getDevice(host: string): BridgePrinterDevice | undefined;
+    function getDevice(ip: string, port: number = DEFAULT_PORT): BridgePrinterDevice | undefined {
+      const exec = regex.host.exec(ip);
+      if (exec == null) return void 0;
+
+      const [_, _ip, _port = port] = exec;
+      return { type: 'wifi', ip: _ip, port: +_port } as BridgePrinterDevice;
+    }
+
+    async function connect(device?: BridgePrinterDevice) {
+      if (device == null) device = _device.value;
+      if (device == null) throw { message: `[无法连接] 打印机配置错误` };
+      try {
+        await window.bridge.printer.connect(device);
+        ip.value = device.ip!!;
+        port.value = device.port!!;
+      } catch (e) {
+        $reset();
+        throw e;
+      }
+    }
+
+    async function disconnect(device?: BridgePrinterDevice) {
+      if (device == null) device = _device.value;
+      if (device == null) throw { message: `[无法断开] 打印机配置错误` };
+      await window.bridge.printer.disconnect(device);
+    }
+
+    function print(tspl: string): Promise<void>;
+    function print(host: string, tspl: string): Promise<void>;
+    function print(hostOrTspl: string, tspl?: string): Promise<void> {
+      if (tspl == null) [tspl, hostOrTspl] = [hostOrTspl, ''];
+
+      const device = hostOrTspl ? getDevice(hostOrTspl) : _device.value;
+      if (device == null) throw { message: `[无法打印] 打印机配置错误` };
+
+      return Bridge.print({ device, tspl }).then((result) => {
+        ip.value = device.ip!!;
+        port.value = device.port!!;
+        return result;
+      });
+    }
+
+    return { ip, port, host, getDevice, connect, disconnect, print, $reset };
+  },
+  {
+    persist: {
+      pick: ['ip', 'port'],
+    },
+  },
+);
+
+export function browserPrint(img?: string) {
+  if (img == null) throw { message: `数据为空` };
+  // 1. 创建打印区域
+  const printArea =
+    document.querySelector<HTMLDivElement>('#print-area') ??
+    ((el: HTMLElement) => {
+      el.id = 'print-area';
+      document.body.appendChild(el);
+      return el;
+    })(document.createElement('div'));
+  printArea.innerHTML = `<img src="${img}" style="width:100%" alt=""/>`;
+
+  // 2. 添加打印专用样式
+  if (!document.querySelector('#print-style')) {
+    const style = document.createElement('style');
+    style.id = 'print-style';
+    style.innerHTML = `
+      @media print {
+        body > *:not(#print-area) { display: none !important; }
+        #print-area { display: block; }
+      }
+      #print-area { display: none; }
+    `;
+    document.head.appendChild(style);
+  }
+
+
+  // 3. 显示打印区域并打印
+  printArea.style.display = 'block';
+  window.print();
+  printArea.style.display = 'none';
+}

+ 11 - 0
src/tools/validator.ts

@@ -0,0 +1,11 @@
+const host = () => {
+  // IP 每段 0-255
+  const ip = '(?:25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)';
+  // 端口 1-65535
+  const port = '(?:([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))';
+  // 拼接整体正则
+  return new RegExp(`^((?:${ip}\\.){3}${ip})(?::${port})?$`);
+};
+export const regex = {
+  host: host(),
+};