ソースを参照

打印发药标签(60mm*40mm)

kumu 1 年間 前
コミット
8bcd382682

+ 3 - 0
.env

@@ -0,0 +1,3 @@
+VUE_APP_C_LODOP_WS=localhost:8000|localhost:18000
+VUE_APP_C_LODOP_HTTP=localhost:8000|localhost:18000
+VUE_APP_C_LODOP_HTTPS=localhost.lodop.net:8443

+ 3 - 1
.env.development

@@ -5,8 +5,10 @@ VUE_APP_TITLE=中药房管理系统
 ENV='development'
 
 # 中药房管理系统/开发环境
-VUE_APP_BASE_API='http://121.43.162.141:8001/prod-api/'
+VUE_APP_BASE_API='http://8.139.252.178:8001/prod-api/'
 VUE_APP_BASE_API_V2='http://10.250.11.48:3030'
 
 # 路由懒加载
 VUE_CLI_BABEL_TRANSPILE_MODULES=true
+# 1仅预览,否则为正常内容
+VUE_APP_C_LODOP_POINT_PREVIEW=1

+ 137 - 0
src/components/print/tag_60_40.vue

@@ -0,0 +1,137 @@
+<script>
+import CLodop from '@/libs/print/CLodop';
+import {getPrint} from '@/api/decoct/recipe';
+import {getDevice} from '@/tools/print.tool';
+import {template60_40} from '@/components/print/template';
+
+export default {
+  name: 'print_tag_60_40',
+  props: {
+    id: {type: [String, Number], required: true},
+  },
+  data() {
+    return {
+      loaded: false,
+      preview: false,
+      total: 1,
+
+      tag: `print-preview_${Date.now()}`,
+      paper: '发药标签',
+
+      model: null,
+      device: null,
+      printing: false,
+    };
+  },
+  computed: {
+    loading() {
+      return !this.loaded || !this.preview;
+    },
+  },
+  mounted() {
+    this.print(true);
+  },
+  methods: {
+    async print(preview = false) {
+      const instance = await CLodop();
+      const model = await this.getModel();
+
+      template60_40.call(instance, model, `${this.paper}`);
+
+      this.device = await getDevice(`paper:${this.paper}`);
+      if (this.device) instance['SET_PRINT_PAGESIZE'](1, 0, 0, this.paper);
+      else {
+        this.device = await getDevice({width: 60, height: 40}, `name:Gprinter GP-1324D`);
+        if (this.device) instance['SET_PRINT_PAGESIZE'](1, '60mm', '40mm', 'CreateCustomPage');
+      }
+      if (this.device) instance['SET_PRINTER_INDEX'](this.device.index);
+
+      instance['SET_PRINT_COPIES'](this.total);
+
+      if (preview) {
+        instance['SET_PREVIEW_WINDOW'](1, 1, 1, 0, 0, `${this.paper}.开始打印`);
+        instance['SET_SHOW_MODE']('PREVIEW_IN_BROWSE', true);
+        instance['SET_PRINT_MODE']('AUTO_CLOSE_PREWINDOW', true);
+        instance['PREVIEW'](this.tag);
+      } else if (this.device) {
+        if (instance['PRINT']()) this.complete();
+        else this.$message.warning(`请检查打印机 (${this.device.name})`);
+      } else {
+        if (instance['PRINTA']()) this.complete();
+        else this.$message.warning(`请检查打印机`);
+      }
+      this.printing = false;
+      this.loaded = true;
+    },
+    delay() {
+      setTimeout(() => {this.preview = true;}, 500);
+    },
+    complete() {
+      this.$message.success(`开始打印`);
+      this.$emit('close', true);
+    },
+
+    async getModel() {
+      if (this.model) return this.model;
+
+      return getPrint({id: this.id}).then((res) => {
+        const data = res.data;
+        this.model = {
+          patient: {
+            name: `${data['name']}`,
+            gender: `${data['sex']}`,
+            birthday: `${data['patientBirthday']}`,
+          },
+          recipe: {
+            count: `${data['number']}`,
+            total: `${data['packageNumber']}`,
+            method: `${data['prescriptionusage']}`,
+            volume: `${data['packageDose']}ml`,
+          },
+          department: [data['department'], data['bedNo']].filter(Boolean).join(' '),
+          record: {
+            title: `${data['medicalName']}(代煎中心)`,
+            date: `${data['createTime']}`,
+            no: `${data['preNo']}`,
+            category: data['preMzZy'] === '1' ? '门诊' : '住院',
+            remark: [data['frequency'], data['medicationTime']].filter(Boolean).join(','),
+          },
+        };
+        this.total = this.model.recipe.total;
+
+        return this.model;
+      });
+    },
+  },
+};
+</script>
+
+<template>
+  <div class="print-preview" v-loading="loading">
+    <div class="top" :style="{backgroundColor: preview ? '#f0f0f0' : 'transparent'}"></div>
+    <iframe :id="tag" @load="delay"></iframe>
+    <div style="display: flex; align-items: center; justify-content: space-evenly;">
+      <el-tooltip class="item" effect="dark" content="打印数量" placement="top">
+        <el-input-number style="width: 120px;" v-model="total" :min="1" :disabled="printing"></el-input-number>
+      </el-tooltip>
+      <el-button type="primary" :loading="printing" @click="printing = true;print()">打印</el-button>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.print-preview {
+  margin: auto;
+  width: 300px;
+
+  .top {
+    height: 40px;
+  }
+
+  iframe {
+    border: none;
+    width: 100%;
+    height: 240px;
+  }
+}
+</style>

+ 62 - 0
src/components/print/template.js

@@ -0,0 +1,62 @@
+import {renderTemplate} from '@/components/print/tools';
+
+export function template60_40(model = {}, title = '') {
+  const template = renderTemplate.bind(this, model);
+
+  const width = 227;
+  const height = 151;
+  const margin = 6;
+
+  this.PRINT_INITA(0, 0, width, height, title);
+  // 宽度按纸张的整宽缩放
+  this.SET_PRINT_MODE('PRINT_PAGE_PERCENT', 'Full-Width');
+  // 设置输出位置以纸张边缘为基点
+  this.SET_PRINT_MODE('POS_BASEON_PAPER', true);
+
+  let x = margin, y = margin;
+  let w = 0, h = 0;
+
+  w = 180;
+  h = 20;
+  this.ADD_PRINT_TEXT(y, x, w, h, template(`{{record.title}}`));
+  this.SET_PRINT_STYLEA(0, 'Bold', 1);
+  w = 70;
+  this.ADD_PRINT_TEXT(x, width - w - margin, w, h, template(`{{record.date}}`));
+  this.SET_PRINT_STYLEA(0, 'Alignment', 3);
+
+  w = 180;
+  y += h;
+  this.ADD_PRINT_TEXT(y, x, w, h, template(`{{patient.name}},{{patient.gender}},{{patient.birthday}}`));
+  x += w;
+  w = width - x - margin;
+  this.ADD_PRINT_TEXT(y, x, w, h, template(`{{record.category}}`));
+  this.SET_PRINT_STYLEA(0, 'Alignment', 3);
+
+
+  x = margin;
+  y = 50 - 4;
+  if (model.record.no) this.ADD_PRINT_BARCODE(y, x, 110, 60, '128Auto', model.record.no);
+  this.SET_PRINT_STYLEA(0, 'FontSize', 6);
+
+
+  x = width - 100 - margin;
+  w = 54;
+  this.SET_PRINT_STYLE('FontSize', 8);
+  this.ADD_PRINT_TEXT(y, x, w, h, template(`剂数:{{recipe.count}}`));
+  this.ADD_PRINT_TEXT(y, width - w - margin, w, h, template(`包数:{{recipe.total}}`));
+  this.SET_PRINT_STYLEA(0, 'Alignment', 3);
+  w = 100;
+  y += h;
+  this.ADD_PRINT_TEXT(y, x, w, h, template(`包装量:{{recipe.volume}}`));
+  y += h;
+  this.ADD_PRINT_TEXT(y, x, w, h, template(`处方用法:{{recipe.method}}`));
+  this.SET_PRINT_STYLE('FontSize', 9);
+
+  x = 6;
+  y += h + 4;
+  w = width - margin * 2;
+  this.SET_PRINT_STYLEA(0, 'FontSize', 8);
+  this.ADD_PRINT_TEXT(y, x, w, h, template(`科室/病区:{{department}}`));
+  y += h - 1;
+  this.ADD_PRINT_TEXT(y, x, w, h, template(`备注:{{record.remark}}`));
+}

+ 21 - 0
src/components/print/tools.js

@@ -0,0 +1,21 @@
+export function renderTemplate(model, template) {
+  // 使用正则表达式匹配模板中的占位符
+  return template.replace(/\{\{(\w+(\.\w+)?)}}/g, (match, key) => {
+    // 根据点分隔符分割键名,逐步访问对象属性
+    const keys = key.split('.');
+    let value = model;
+    for (let k of keys) {
+      if (value) { value = value[k]; } else { return match; }
+    }
+    return value == null ? '' : value;
+  });
+}
+
+export function appendLocatingPoint(width, height, size, preview = process.env.VUE_APP_C_LODOP_POINT_PREVIEW) {
+  this.SET_PRINT_STYLE('PreviewOnly', preview || 1);
+  this.ADD_PRINT_SHAPE(4, 0, 0, size, size, 0, 1, '#000000');
+  this.ADD_PRINT_SHAPE(4, 0, width - size, size, size, 0, 1, '#000000');
+  this.ADD_PRINT_SHAPE(4, height - size, 0, size, size, 0, 1, '#000000');
+  this.ADD_PRINT_SHAPE(4, height - size, width - size, size, size, 0, 1, '#000000');
+  this.SET_PRINT_STYLE('PreviewOnly', 0);
+}

+ 55 - 0
src/libs/print/CLodop.js

@@ -0,0 +1,55 @@
+const FILENAME = '/CLodopfuncs.js';
+
+function loadByScript(url, protocol = 'http://', name = FILENAME) {
+  if (!url.startsWith(protocol)) url = `${protocol}${url}`;
+  if (!url.endsWith(name)) url = `${url}${name}`;
+  if (window['getCLodop']) return Promise.resolve();
+  return new Promise((resolve, reject) => {
+    const el = document.createElement('script');
+    el.src = url;
+    el.addEventListener('load', resolve);
+    el.addEventListener('error', reject);
+    document.head.appendChild(el);
+  });
+}
+
+function loadByWebSocket(url, protocol = 'ws://', name = FILENAME) {
+  if (!url.startsWith(protocol)) url = `${protocol}${url}`;
+  if (!url.endsWith(name)) url = `${url}${name}`;
+  if (window['getCLodop']) return Promise.resolve();
+  return new Promise((resolve, reject) => {
+    const ws = new WebSocket(url);
+    ws.addEventListener('open', () => { setTimeout(reject, 200); });
+    ws.addEventListener('error', reject);
+    ws.addEventListener('message', (e) => {
+      if (!window['getCLodop']) eval(e.data);
+      resolve();
+    });
+  });
+}
+
+export default function load(options) {
+  const {ws, http, https, licenses} = Object.assign({
+    ws: (process.env.VUE_APP_C_LODOP_WS || 'localhost:8000|localhost:18000').split('|'),
+    http: (process.env.VUE_APP_C_LODOP_HTTP || 'localhost:8000|localhost:18000').split('|'),
+    https: (process.env.VUE_APP_C_LODOP_HTTPS || 'localhost.lodop.net:8443').split('|'),
+    licenses: (process.env.VUE_APP_C_LODOP_LICENSES || '').split('|'),
+  }, options);
+
+  const disable = location.protocol === 'https:';
+
+  return Promise.any(ws.map((url) => loadByWebSocket(url)))
+    .catch(() => Promise.any(https.map(url => loadByScript(url, 'https://'))))
+    .catch(e => {
+      if (disable) throw e;
+      return Promise.any(http.map(url => loadByScript(url)));
+    })
+    .then(() => {
+      const instance = window['getCLodop']();
+      for (const license of licenses) {
+        const value = license.split('_');
+        instance['SET_LICENSES'](...value);
+      }
+      return instance;
+    });
+}

+ 47 - 0
src/tools/print.tool.js

@@ -0,0 +1,47 @@
+let c_lodop = async function (options = {}) {
+  const module = await import('@/libs/print/CLodop.js');
+
+  const instance = await module.default(options);
+  c_lodop = () => Promise.resolve(instance);
+
+  return instance;
+};
+
+export async function getDevices() {
+  const LODOP = await c_lodop();
+  return Array.from({length: LODOP['GET_PRINTER_COUNT']()}, (_, i) => {
+    return {
+      name: LODOP['GET_PRINTER_NAME'](`${i}:DriverName`),
+      index: i,
+      papers: LODOP['GET_PAGESIZES_LIST'](i, '\n').split('\n'),
+      size: +LODOP['GET_PRINTER_NAME'](`${i}:PaperSize`),
+      width: +LODOP['GET_PRINTER_NAME'](`${i}:PaperWidth`),
+      height: +LODOP['GET_PRINTER_NAME'](`${i}:PaperLength`),
+      dpi: +LODOP['GET_PRINTER_NAME'](`${i}:PrintQuality`),
+      form: LODOP['GET_PRINTER_NAME'](`${i}:FormName`),
+    };
+  });
+}
+
+export async function getDevice(...priority) {
+  const devices = await getDevices();
+  let device;
+  for (let item of priority) {
+    if (typeof item === 'string') {
+      const [key, value] = item.split(':');
+      item = {[key]: value};
+    }
+    device = devices.find(device => Object.entries(item).every(([key, value]) => {
+      if (key === 'paper') return device['papers'].includes(value);
+      if (key === 'name') return device.name.startsWith(value);
+      if (key === 'width' || key === 'height') value *= 10; /* 传入 mm,比较的是 0.1mm */
+      return device[key] === value;
+    }));
+    if (device) break;
+  }
+  return device;
+}
+
+export default function (options = {}) {
+  return c_lodop(options);
+}

+ 10 - 9
src/views/decoct/recipe/index.vue

@@ -164,8 +164,11 @@
       <detail-page :id="id" ref="detailDialog" @success="handleCloseOption();getList()" />
     </el-dialog>
     <!-- 打印预览 -->
-    <el-dialog title="打印预览" :visible.sync="showPrint" width="35%" :before-close="handleCloseOption" append-to-body center>
-      <print :id="id" ref="printDialog" />
+    <el-dialog title="打印预览" :visible.sync="showPrint" width="500px"
+               append-to-body center
+               :before-close="handleCloseOption"
+               @closed="showPrintContent=false">
+      <print v-if="showPrintContent" :id="id" @close="showPrint = false; $event && getList()"></print>
     </el-dialog>
   </div>
 </template>
@@ -175,7 +178,7 @@ import { getSchemeList } from "@/api/decoct/scheme";
 import { getRecipeList, getHospitalList } from "@/api/decoct/recipe";
 import editOption from "./components/editOption.vue";
 import detailPage from "./components/detailPage.vue";
-import print from "./components/print.vue";
+import print from "@/components/print/tag_60_40.vue";
 
 // 防抖函数
 function debounce(fn, delay) {
@@ -223,6 +226,7 @@ export default {
       showOption: false, // 修改方案弹窗
       showDetail: false, // 详情弹窗
       showPrint: false, // 打印页面弹窗
+      showPrintContent: false, // 打印页面弹窗
       typeOptions: [], // 剂型
       schemeOptions: [],  // 煎药方案
       id: '',
@@ -280,12 +284,9 @@ export default {
         })
       }
     },
-    handlePrint(id) {
-      this.showPrint = true
-      this.id = id
-      this.$nextTick(() => {
-        this.$refs.printDialog.getInfo()
-      })
+    async handlePrint(id) {
+      this.id = id;
+      this.showPrint = this.showPrintContent = true;
     },
     // 分页改变
     handleCurrentChange(e) {