Просмотр исходного кода

Merge branch 'feature/print' into develop

kumu 1 год назад
Родитель
Сommit
51b8d85bac

+ 9 - 0
.env

@@ -0,0 +1,9 @@
+VUE_APP_SF_EXPRESS_APPID=LZKJ38MQCGN
+VUE_APP_SF_EXPRESS_APPNAME=RP4
+
+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
+
+# 顺丰签名
+VUE_APP_C_LODOP_LICENSES='顺丰科技有限公司_A8014B09DC3900222D3047E9942A8F3504D_順豐科技有限公司_EA15AFAF29B939797009E405CDEB043768A|THIRD LICENSE__Sf Technology Co., Ltd._F5BD5E2D3083D6F7FA2FF6C5DFEB3740F52'

+ 6 - 1
.env.development

@@ -5,8 +5,13 @@ 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
+
+# 顺丰调试环境
+VUE_APP_SF_EXPRESS_ENV=pro
+# 1仅预览,否则为正常内容
+VUE_APP_C_LODOP_POINT_PREVIEW=1

+ 2 - 0
.env.production

@@ -27,3 +27,5 @@ VUE_APP_BASE_API_V2='http://124.112.64.166:2020'
 
 VUE_APP_BASE_API='/prod-api/'
 
+VUE_APP_SF_EXPRESS_ENV=pro
+

+ 8 - 0
src/api/prescription/prescriptionAudit.js

@@ -19,6 +19,14 @@ export function selectOrderDetail(query) {
   })
 }
 
+export function getExpressRecordParams(data) {
+  return request({
+    url: '/yfc-admin/prescriptionManage/expressSfPrint',
+    method: 'post',
+    data,
+  });
+}
+
 
 // 释放待审核处方
 export function releaseOrder(query) {

+ 17 - 0
src/api/prescription/prescriptionCore.js

@@ -101,6 +101,14 @@ export function addPrescriptionCore(data) {
   })
 }
 
+export function addPrescriptionCore2(model) {
+  return request({
+    url: '/yfc-admin/prescriptionManage/addPrescription',
+    method: 'post',
+    data: model
+  });
+}
+
 // 修改处方审核
 export function updatePrescriptionCore(data) {
   return request({
@@ -144,3 +152,12 @@ export function addNum(id) {
 
   })
 }
+
+// 获取快递面单参数
+export function getExpressRecordParams(id, type = 'sf') {
+  return request({
+    url: '/yfc-admin/prescriptionManage/expressSfPrint',
+    method: 'post',
+    data: {prescriptionCoreId: id}
+  })
+}

+ 96 - 0
src/components/print/express_75.vue

@@ -0,0 +1,96 @@
+<script>
+import {sf_express_print} from '@/tools/print.tool';
+import {getExpressRecordParams} from '@/api/prescription/prescriptionAudit';
+
+export default {
+  name: 'print_express_75',
+  props: {
+    id: {type: [String, Number], required: true},
+  },
+  data() {
+    return {
+      loaded: false,
+      preview: true,
+      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) {
+      try {
+        await this.getModel();
+        if (preview) {
+
+        } else {
+          this.printing = true;
+          await sf_express_print(this.model);
+        }
+      } catch (e) {}
+      this.printing = false;
+      this.loaded = true;
+    },
+    complete() {
+      this.$message.success(`开始打印`);
+      this.$emit('close', true);
+    },
+
+    async getModel() {
+      if (this.model) return this.model;
+      return getExpressRecordParams({prescriptionCoreId: this.id}).then((res) => {
+        console.log(res);
+        this.model = res.data;
+        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-button style="width: 100%;" type="primary" :loading="printing" @click="printing = true;print()">
+        打印
+      </el-button>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.print-preview {
+  margin: auto;
+  width: 559px + 100px;
+  display: flex;
+  flex-direction: column;
+
+  .top {
+    height: 40px;
+  }
+
+  iframe {
+    border: none;
+    width: 100%;
+    flex: auto;
+  }
+
+  > div {
+    flex: none;
+  }
+}
+</style>

+ 180 - 0
src/components/print/recipe_A5.vue

@@ -0,0 +1,180 @@
+<script>
+import CLodop from '@/libs/print/CLodop';
+import {getDevice} from '@/tools/print.tool';
+import {templateA5} from '@/components/print/template';
+import {selectOrderDetail} from '@/api/prescription/prescriptionAudit';
+
+export default {
+  name: 'print_recipe_a5',
+  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();
+
+      templateA5.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: 559, height: 794}, `name:Canon LBP`);
+        if (this.device) instance['SET_PRINT_PAGESIZE'](1, '148mm', '210mm', '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 selectOrderDetail({id: this.id}).then((res) => {
+        const data = res.data;
+        const sign = (index) => { try {return data.operateList[index].operater;} catch (e) {} };
+        this.model = {
+          patient: {
+            name: `${data['name']}`,
+            gender: `${data['sex']}`,
+            arg: `${data['args']}岁`,
+            birthday: `${data['patientBirthday']}`,
+            phone: `${data['contactNumber']}`,
+          },
+          recipe: {
+            date: `${data['prescriptionTime']}`,
+            type: {1: '中药处方', 2: '中药制剂'}[data.type] || '',
+            count: data['number'],
+            category: data['dosageForm'],
+            method: data['prescriptionusage'],
+            volume: data['concentration'] && `每次${data['concentration']}`,
+            frequency: data['frequency'],
+            frequencyTime: data['medicationTime'],
+            decoction: data['isBehalf'] === '1' || +data['daijianNumber'] > 0,
+
+            delivery: data['expressExecutor'],
+            address: [data['province'], data['city'], data['region'], data['address']].filter(Boolean).join(''),
+
+            medicineFees: data['prescriptionSum'],
+            decoctionFees: data['daijianCost'],
+            deliveryFees: data['distributionCost'],
+            totalFees: data['prescriptionTotalSum'],
+
+            medicines: Array.isArray(data['detailList']) ? data['detailList'].map(item => {
+              return {
+                name: item['matName'],
+                dosage: item['matDose'],
+                unit: item['matUnitName'],
+                usage: item['matUsageName'],
+              };
+            }) : [],
+          },
+          department: [data['department']].filter(Boolean).join(' '),
+          diagnosis: [data['disName']].filter(Boolean).join(' '),
+          record: {
+            title: `${data['yljgName']}(处方笺)`,
+            date: `${data['printTime']}`,
+            no: `${data['preNo']}`,
+            category: data['preMzZy'] === '1' ? '门诊' : '住院',
+            remark: '1、本处方当日有效;\r\n2、取药时请您当面核对;\r\n3、延长处方用量时间原由:慢性病 其他老年病 外地 其他',
+          },
+          recordNo: data['recordNo'],
+          bedNo: data['bedNo'],
+          sign: {
+            field1: sign(1 - 1),
+            field2: sign(2 - 1),
+            field3: sign(3 - 1),
+            field4: sign(4 - 1),
+            field5: sign(5 - 1),
+            field6: sign(6 - 1),
+            field7: sign(7 - 1),
+            field8: sign(8 - 1),
+          },
+          field1: {1: '配药', 2: '煎药', 3: '发药', 4: '配送'}[data.state],
+        };
+        if (this.model.field1) this.model.field1 = `当前处方状态:${this.model.field1}`;
+        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-button style="width: 100%;" type="primary" :loading="printing" @click="printing = true;print()">
+        打印
+      </el-button>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.print-preview {
+  margin: auto;
+  width: 559px + 100px;
+  display: flex;
+  flex-direction: column;
+
+  .top {
+    height: 40px;
+  }
+
+  iframe {
+    border: none;
+    width: 100%;
+    flex: auto;
+  }
+
+  > div {
+    flex: none;
+  }
+}
+</style>

+ 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>

+ 133 - 0
src/components/print/tag_80_50.vue

@@ -0,0 +1,133 @@
+<script>
+import CLodop from '@/libs/print/CLodop';
+import {getDevice} from '@/tools/print.tool';
+import {template80_50} from '@/components/print/template';
+import {selectOrderDetail} from '@/api/prescription/prescriptionAudit';
+
+export default {
+  name: 'print_tag_80_50',
+  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();
+
+      template80_50.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 selectOrderDetail({id: this.id}).then((res) => {
+        const data = res.data;
+        this.model = {
+          patient: {
+            name: data['name'],
+            gender: data['sex'],
+            birthday: data['patientBirthday'],
+            phone: data['contactNumber'],
+          },
+          recipe: {
+            count: data['number'],
+            method: data['prescriptionusage'],
+            decoction: data['isBehalf'] === '1' ? '是' : '否',
+          },
+          department: [data['department']].filter(Boolean).join(' '),
+          record: {
+            title: `${data['yljgName']}(中药房)`,
+            date: data['printTime'],
+            no: data['preNo'],
+            category: data['preMzZy'] === '1' ? '门诊' : '住院',
+            remark: [data['pharmacistsremarks']].filter(Boolean).join(','),
+          },
+        };
+
+        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-button style="width: 100%;" type="primary" :loading="printing" @click="printing = true;print()">打印</el-button>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.print-preview {
+  margin: auto;
+  width: 360px;
+
+  .top {
+    height: 40px;
+  }
+
+  iframe {
+    border: none;
+    width: 100%;
+    height: 280px;
+  }
+}
+</style>

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

@@ -0,0 +1,452 @@
+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}}`));
+}
+
+export function template80_50(model = {}, title = '') {
+  const template = renderTemplate.bind(this, model);
+
+  const width = 302;
+  const height = 189;
+  const margin = 12;
+
+  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;
+
+  y -= 2;
+  w = width - margin * 2;
+  h = 30;
+  this.ADD_PRINT_TEXT(y, x, w, h, template(`{{record.title}}`));
+  this.SET_PRINT_STYLEA(0, 'Bold', 1);
+  this.SET_PRINT_STYLEA(0, 'Alignment', 2);
+
+  w = 70;
+  h = 20;
+  y = 40;
+  x = width - w - margin;
+  this.ADD_PRINT_TEXT(y, x, w, h, template(`{{record.category}}`));
+  this.SET_PRINT_STYLEA(0, 'Alignment', 3);
+  w = x - margin;
+  this.ADD_PRINT_TEXT(y, margin, w, h, template(`{{patient.name}},{{patient.gender}},{{patient.birthday}}`));
+
+  x = margin;
+  y += h;
+  if (model.record.no) this.ADD_PRINT_BARCODE(y, x, 145, 60, '128Auto', model.record.no);
+  this.SET_PRINT_STYLEA(0, 'FontSize', 6);
+
+  this.SET_PRINT_STYLE('FontSize', 8);
+  w = 130;
+  x = width - w - margin;
+  y += 4;
+  this.ADD_PRINT_TEXT(y, x, w, h, template(`手机号:{{patient.phone}}`));
+  y += h;
+  this.ADD_PRINT_TEXT(y, x, w, h, template(`处方用法:{{recipe.method}}`));
+  y += h;
+  w = 55;
+  this.ADD_PRINT_TEXT(y, x, w, h, template(`剂数:{{recipe.count}}`));
+  x += w;
+  this.ADD_PRINT_TEXT(y, x, width - x - margin, h, template(`是否代煎:{{recipe.decoction}}`));
+  this.SET_PRINT_STYLEA(0, 'Alignment', 3);
+  this.SET_PRINT_STYLE('FontSize', 9);
+
+  x = margin;
+  y += h;
+  w = width - margin * 2;
+  this.ADD_PRINT_TEXT(y, x, w, h, template(`科室/病房:{{department}}`));
+  y += h;
+  h += 6;
+  this.ADD_PRINT_TEXT(y, x, w, h, template(`药师备注:{{record.remark}}`));
+  y += h;
+  h = 20;
+  this.ADD_PRINT_TEXT(y, x, w, h, template(`打印时间:{{record.date}}`));
+  this.SET_PRINT_STYLEA(0, 'Alignment', 2);
+}
+
+/**
+ * A5 纸 96 PPI (559px * 794px)
+ *
+ * @param model
+ * @param title
+ */
+export function templateA5(model = {}, title = '') {
+  const template = renderTemplate.bind(this, model);
+
+  const width = 559;
+  const height = 794;
+  const margin = 12;
+
+  this.PRINT_INITA(0, 0, width, height, title);
+  // 宽度按纸张的整宽缩放
+  this.SET_PRINT_MODE('PRINT_PAGE_PERCENT', 'Full-Height');
+  // 设置输出位置以纸张边缘为基点
+  this.SET_PRINT_MODE('POS_BASEON_PAPER', true);
+
+  const chunks = (function (array, size = 32) {
+    let result = [];
+    for (let i = 0; i < array.length; i += size) {result.push(array.slice(i, i + size));}
+    return result;
+  })(model.recipe['medicines']);
+  if (!chunks.length) chunks.push([]);
+  const single = chunks.length === 1;
+
+  for (const chunk of chunks) {
+    // 绘制头部
+    this.ADD_PRINT_TEXT(margin, margin, 310, 30, template(`{{record.title}}`));
+    this.SET_PRINT_STYLEA(0, 'FontSize', 16);
+    this.SET_PRINT_STYLEA(0, 'Alignment', 2);
+    this.SET_PRINT_STYLEA(0, 'Bold', 1);
+    this.ADD_PRINT_TEXT(47, 12, 310, 20, template(`{{recipe.type}}`));
+    this.SET_PRINT_STYLEA(0, 'Alignment', 2);
+    if (model.record.no) this.ADD_PRINT_BARCODE(12, 331, 216, 60, '128Auto', model.record.no);
+
+    // 绘制顶部信息
+    let x = 0, y = 80, h = 20;
+    let rows = [
+      [
+        {left: margin, width: 138, height: 20, text: template(`姓名:{{patient.name}}`)},
+        {width: 60, height: 20, text: template(`性别:{{patient.gender}}`)},
+        {width: 80, height: 20, text: template(`年龄:{{patient.age}}`)},
+        {width: 120, height: 20, text: template(`电话:{{patient.phone}}`)},
+      ],
+      [
+        {left: margin, width: 198, height: 20, text: template(`就诊科室:{{department}}`)},
+        {width: 80, height: 20, text: template(`床号:{{bedNo}}`)},
+        {width: 120, height: 20, text: template(`病历号:{{recordNo}}`)},
+      ],
+      [
+        {left: margin, width: 350, height: 20, text: template(`临床诊断:{{diagnosis}}`)},
+        {leftOffset: 2, width: 350, height: 20, text: template(`开方时间:{{recipe.date}}`)},
+      ],
+      [
+        {left: margin, width: 400, height: 20, text: template(`联系地址:{{recipe.address}}`)},
+        {
+          leftOffset: 2, width: 134, height: 20, text: template(`{{recipe.delivery}}`), style: {
+            Alignment: 3,
+          },
+        },
+      ],
+    ];
+    rows.forEach((row, r) => {
+      row.forEach((col, c) => {
+        if (col.top != null) y = col.top;
+        else if (r && c === 0) { y += (h + 2); }
+
+        if (col.left != null) x = col.left;
+        else if (c) {x += row[c - 1].width + (col.leftOffset || 0);}
+
+        this.ADD_PRINT_TEXT(y, x, col.width, col.height, col.text);
+        for (const [prop, value] of Object.entries(col.style || {})) {
+          this.SET_PRINT_STYLEA(0, prop, value);
+        }
+      });
+    });
+
+    this.ADD_PRINT_TEXT(80, 410, 128, 42, template(`{{field1}}`));
+    this.SET_PRINT_STYLEA(0, 'Alignment', 3);
+    this.SET_PRINT_STYLEA(0, 'Italic', 1);
+    this.SET_PRINT_STYLEA(0, 'Underline', 1);
+
+
+    // 分割线
+    this.ADD_PRINT_SHAPE(4, 178, 12, 535, 1, 0, 1, '#000000');
+    // RP
+    this.ADD_PRINT_TEXT(192, 12, 50, 30, 'Rp:');
+    this.SET_PRINT_STYLEA(0, 'FontSize', 16);
+    this.SET_PRINT_STYLEA(0, 'Bold', 1);
+
+    // 绘制药品
+    y = 230 - 20 - 2;
+    chunk.forEach((medicine, i) => {
+      const template = renderTemplate.bind(this, medicine);
+      if (i % 2) {
+        x = 291;
+      } else {
+        y += 20 + 2;
+        x = 48;
+      }
+      this.ADD_PRINT_TEXT(y, x, 120, 20, template(`{{name}}`));
+      this.SET_PRINT_STYLEA(0, 'Alignment', 2);
+      x += 120;
+      this.ADD_PRINT_TEXT(y, x, 50, 20, template(`{{dosage}}{{unit}}`));
+      this.SET_PRINT_STYLEA(0, 'Alignment', 3);
+      x += 50 + 2;
+      this.ADD_PRINT_TEXT(y, x, 48, 16, template(`{{usage}}`));
+      this.SET_PRINT_STYLEA(0, 'FontSize', 6);
+    });
+
+    // 绘制处方信息
+    y = Math.max(560, y);
+    y += 20 + 8 * 2;
+    this.SET_PRINT_STYLE('Alignment', 2);
+    this.ADD_PRINT_TEXT(y, margin, 111, 20, template(`剂数:{{recipe.count}} ${model.recipe.decoction ? '(代煎)' : ''}`));
+    this.ADD_PRINT_TEXT(y, 120, 100, 20, template(`{{recipe.category}} ${model.recipe.method ? '({{recipe.method}})' : ''}`));
+    this.ADD_PRINT_TEXT(y, 229, 100, 20, template(`{{recipe.volume}}`));
+    this.ADD_PRINT_TEXT(y, 339, 100, 20, template(`{{recipe.frequency}}`));
+    this.ADD_PRINT_TEXT(y, 448, 100, 20, template(`{{recipe.frequencyTime}}`));
+    this.ADD_PRINT_SHAPE(4, 619, 12, 535, 1, 0, 1, '#000000');
+
+    // 绘制矩形
+    this.ADD_PRINT_RECT(620 + 12, 12, 240, 70, 0, 1);
+    // 第一横行线
+    this.ADD_PRINT_SHAPE(4, 654, 12, 240, 1, 0, 1, '#000000');
+    // 第二横行线
+    this.ADD_PRINT_SHAPE(4, 679, 12, 240, 1, 0, 1, '#000000');
+    // 第二行 竖线
+    this.ADD_PRINT_SHAPE(4, 655, 132, 1, 24, 0, 1, '#000000');
+    this.ADD_PRINT_TEXT(637, 13, 238, 12, template(`药品金额:¥{{recipe.medicineFees}}`));
+    this.ADD_PRINT_TEXT(661, 13, 118, 12, template(`煎药费:¥{{recipe.decoctionFees}}`));
+    this.ADD_PRINT_TEXT(661, 133, 118, 12, template(`配送费:¥{{recipe.deliveryFees}}`));
+    this.ADD_PRINT_TEXT(685, 13, 238, 12, template(`总金额:¥{{recipe.totalFees}}`));
+    this.SET_PRINT_STYLE('Alignment', 1);
+    this.SET_PRINT_STYLE('FontSize', 8);
+
+    y = 620 + 12 + 4;
+    rows = [
+      [
+        {left: 266, text: template(`开方:{{sign.field1}}`)},
+        {left: 408, text: template(`审核:{{sign.field2}}`)},
+      ],
+      [
+        {left: 266, text: template(`调配:{{sign.field3}}`)},
+        {left: 408, text: template(`复核:{{sign.field4}}`)},
+      ],
+      [
+        {left: 266, text: template(`浸泡:{{sign.field5}}`)},
+        {left: 408, text: template(`煎煮:{{sign.field6}}`)},
+      ],
+      [
+        {left: 266, text: template(`打包:{{sign.field7}}`)},
+        {left: 408, text: template(`发药:{{sign.field8}}`)},
+      ],
+    ];
+    for (const row of rows) {
+      for (const col of row) {
+        this.ADD_PRINT_TEXT(y, col.left, 140, 16, col.text);
+        this.ADD_PRINT_TEXT(y, col.left, 140, 16, col.text);
+      }
+      y += 16 + 2;
+    }
+    this.SET_PRINT_STYLE('FontSize', 9);
+    this.ADD_PRINT_SHAPE(4, 714, 12, 535, 1, 0, 1, '#000000');
+
+    if (model.record.remark) {
+      this.ADD_PRINT_TEXT(725, 12, 40, 16, '注:');
+      this.SET_PRINT_STYLEA(0, 'Bold', 1);
+      this.SET_PRINT_STYLEA(0, 'Vorient', 1);
+      this.ADD_PRINT_TEXT(725, 48, 500, 55, template(`{{record.remark}}`));
+      this.SET_PRINT_STYLEA(0, 'Vorient', 1);
+    }
+
+    if (!single) this.NEWPAGEA();
+  }
+
+}
+
+/**
+ * 药品明细清单
+ * @param model
+ * @param title
+ */
+export function template72(model, title) {
+  const template = renderTemplate.bind(this, model);
+
+  const width = 272;
+  const height = 794;
+  const margin = 0;
+
+  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 = 70;
+  h = 20;
+  this.ADD_PRINT_TEXT(y, x, w, h, template(`{{recipe.delivery}}`));
+  this.SET_PRINT_STYLEA(0, 'Alignment', 2);
+  this.SET_PRINT_STYLEA(0, 'Bold', 1);
+  x += w;
+  h = 30;
+  this.ADD_PRINT_TEXT(y, x, width - x - margin, h, template(`{{record.title}}`));
+  this.SET_PRINT_STYLEA(0, 'FontSize', 16);
+  this.SET_PRINT_STYLEA(0, 'Alignment', 2);
+  this.SET_PRINT_STYLEA(0, 'Bold', 1);
+  x = margin;
+  y += h;
+  h = 36;
+  this.ADD_PRINT_TEXT(y, x, width - x - margin, h, template(`{{recipe.address}}`));
+  this.SET_PRINT_STYLEA(0, 'Underline', 1);
+  x = margin;
+  y += h;
+  w = 118;
+  h = 20;
+  this.ADD_PRINT_TEXT(y, x, w, h, template(`序号:{{bedNo}}`));
+  x += w;
+  this.ADD_PRINT_TEXT(y, x, width - x - margin, h, template(`处方号:{{record.no}}`));
+
+  y += h;
+  x = margin;
+
+  this.ADD_PRINT_TEXT(y, x, 118, h, template(`姓名:{{patient.name}}`));
+  this.ADD_PRINT_TEXT(y, 118, 75, 20, template(`{{recipe.method}}`));
+  this.SET_PRINT_STYLEA(0, 'Alignment', 2);
+  this.SET_PRINT_STYLEA(0, 'Underline', 1);
+  this.ADD_PRINT_TEXT(y, 197, 75, 20, template(`{{recipe.decoction}}`));
+  this.SET_PRINT_STYLEA(0, 'Alignment', 2);
+  this.SET_PRINT_STYLEA(0, 'Underline', 1);
+
+  y += h;
+  x = margin;
+  h = 96;
+  if (model.record.no) this.ADD_PRINT_BARCODE(y, x, h, h, 'QRCode', model.record.no);
+
+  x = 94;
+  h = 45 * 2;
+  this.ADD_PRINT_TEXT(y, x, 178, h, template(`备注:{{record.remark}}`));
+
+  x = margin;
+  y += h;
+
+  w = 72;
+  h = 20;
+  this.ADD_PRINT_TEXT(y, x, w, h, `药味:${model.recipe.medicines.length}}`);
+  x += w;
+  w = 100;
+  this.ADD_PRINT_TEXT(y, x, w, h, template(`帖重:{{recipe.unitWeight}}`));
+  this.SET_PRINT_STYLEA(0, 'Alignment', 3);
+  x += w;
+  this.ADD_PRINT_TEXT(y, x, w, h, template(`总重:{{recipe.totalWeight}}`));
+  this.SET_PRINT_STYLEA(0, 'Alignment', 2);
+
+  x = margin;
+  y += h;
+  w = 100;
+  this.ADD_PRINT_TEXT(y, x, w, h, template(`开单:{{doctor}}`));
+  x += w;
+  this.ADD_PRINT_TEXT(y, x, width - x - margin, h, template(`共 {{recipe.count}}帖 {{recipe.total}}包`));
+  this.SET_PRINT_STYLEA(0, 'Alignment', 3);
+
+  x = margin;
+  y += h;
+  w = 178;
+  h = 20;
+  this.ADD_PRINT_TEXT(y, x, w, h, [
+    template(`{{recipe.volume}}`),
+    template(`{{recipe.frequency}}`),
+    template(`{{recipe.frequencyTime}}`),
+  ].filter(Boolean).join(','));
+
+  w = 100;
+  this.ADD_PRINT_TEXT(y, width - w - margin, w, h, '装量:  ML');
+  this.SET_PRINT_STYLEA(0, 'Alignment', 3);
+
+  this.ADD_PRINT_SHAPE(4, 255, 0, 278, 1, 0, 1, '#000000');
+  this.ADD_PRINT_TEXT(265, 0, 60, 20, '库位号');
+  this.SET_PRINT_STYLEA(0, 'Alignment', 2);
+  this.ADD_PRINT_TEXT(265, 53, 86, 20, '药品名');
+  this.SET_PRINT_STYLEA(0, 'Alignment', 2);
+  this.ADD_PRINT_TEXT(265, 136, 36, 20, '单帖');
+  this.SET_PRINT_STYLEA(0, 'Alignment', 3);
+  this.ADD_PRINT_TEXT(265, 166, 36, 20, '总量');
+  this.SET_PRINT_STYLEA(0, 'Alignment', 3);
+  this.ADD_PRINT_TEXT(265, 196, 36, 20, '单位');
+  this.SET_PRINT_STYLEA(0, 'Alignment', 3);
+  this.ADD_PRINT_TEXT(265, 228, 44, 20, '特煎');
+  this.SET_PRINT_STYLEA(0, 'Alignment', 3);
+  this.ADD_PRINT_SHAPE(4, 285, 0, 278, 1, 0, 1, '#000000');
+  y = 285 + 12;
+
+  this.SET_PRINT_STYLE('FontSize', 8);
+  model.recipe.medicines.forEach((medicine, i) => {
+    const template = renderTemplate.bind(this, medicine);
+
+    x = margin;
+    w = 54;
+    h = 30;
+    this.ADD_PRINT_TEXT(y, x, w, h, template(`{{mark}}`));
+    x += w - 1;
+    w = 86;
+    this.ADD_PRINT_TEXT(y, x, w, h, template(`{{name}}`));
+    this.SET_PRINT_STYLEA(0, 'Alignment', 2);
+    this.ADD_PRINT_TEXT(y, 132, 40, h, template(`{{dosage}}`));
+    this.SET_PRINT_STYLEA(0, 'Alignment', 3);
+    this.ADD_PRINT_TEXT(y, 164, 40, h, template(`{{total}}`));
+    this.SET_PRINT_STYLEA(0, 'Alignment', 3);
+    this.ADD_PRINT_TEXT(y, 200, 22, h, template(`{{unit}}`));
+    this.SET_PRINT_STYLEA(0, 'Alignment', 3);
+    this.ADD_PRINT_TEXT(y, 222, 50, h, template(`{{usage}}`));
+    this.SET_PRINT_STYLEA(0, 'Alignment', 3);
+    y += h + 2;
+  });
+  this.SET_PRINT_STYLE('FontSize', 9);
+  y += 12;
+  this.ADD_PRINT_SHAPE(4, y, 0, 278, 1, 0, 1, '#000000');
+
+}

+ 179 - 0
src/components/print/ticket_72.vue

@@ -0,0 +1,179 @@
+<script>
+import CLodop from '@/libs/print/CLodop';
+import {getDevice} from '@/tools/print.tool';
+import {template72} from '@/components/print/template';
+import {selectOrderDetail} from '@/api/prescription/prescriptionAudit';
+import {bignumber, chain, multiply} from 'mathjs';
+
+export default {
+  name: 'print_ticket_72',
+  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();
+
+      template72.call(instance, model, `${this.paper}`);
+
+      this.device = await getDevice(`paper:${this.paper}`);
+      if (this.device) instance['SET_PRINT_PAGESIZE'](3, 0, 0, this.paper);
+      else {
+        this.device = await getDevice(`name:GP-C80180 Series`);
+        instance['SET_PRINT_PAGESIZE'](3, '72mm', 0, '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 selectOrderDetail({id: this.id}).then((res) => {
+        const data = res.data;
+        const count = bignumber(data['number'] || 0);
+        this.model = {
+          patient: {
+            name: `${data['name']}`,
+            gender: `${data['sex']}`,
+            arg: `${data['args']}岁`,
+            birthday: `${data['patientBirthday']}`,
+            phone: `${data['contactNumber']}`,
+          },
+          recipe: {
+            date: `${data['prescriptionTime']}`,
+            type: {1: '中药处方', 2: '中药制剂'}[data.type] || '',
+            count: data['number'],
+            total: data['packageNumber'],
+            category: data['dosageForm'],
+            method: data['prescriptionusage'],
+            volume: data['concentration'] && `每次${data['concentration']}`,
+            frequency: data['frequency'],
+            frequencyTime: data['medicationTime'],
+            decoction: (data['isBehalf'] === '1' || +data['daijianNumber'] > 0) ? '代煎' : '自煎',
+
+            delivery: data['expressExecutor'],
+            address: [data['province'], data['city'], data['region'], data['address']].filter(Boolean).join(''),
+
+            unitWeight: '',
+            totalWeight: '',
+
+            medicines: Array.isArray(data['detailList']) ? data['detailList'].map(item => {
+              return {
+                mark: item['locatorNum'],
+                name: item['matName'],
+                dosage: item['matDose'],
+                unit: item['matUnitName'],
+                usage: item['matUsageName'],
+                total: +multiply(count, bignumber(item['matDose'] || 0)).toFixed(2),
+              };
+            }) : [],
+          },
+          doctor: data['doctor'],
+          department: [data['department']].filter(Boolean).join(' '),
+          diagnosis: [data['disName']].filter(Boolean).join(' '),
+          record: {
+            title: `${data['yljgName']}`,
+            date: data['printTime'],
+            no: data['preNo'],
+            category: data['preMzZy'] === '1' ? '门诊' : '住院',
+            remark: [data['pharmacistsremarks']].filter(Boolean).join(','),
+          },
+          recordNo: data['recordNo'],
+          bedNo: data['bedNo'],
+        };
+
+        let unitWeight = chain(bignumber(0));
+        for (const medicine of this.model.recipe.medicines) {
+          unitWeight = unitWeight.add(bignumber(medicine.dosage));
+        }
+
+        this.model.recipe.unitWeight = +unitWeight.valueOf().toFixed(2);
+        this.model.recipe.totalWeight = +unitWeight.multiply(count).valueOf().toFixed(2);
+
+        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-button style="width: 100%;" type="primary" :loading="printing" @click="printing = true;print()">打印
+      </el-button>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.print-preview {
+  margin: auto;
+  width: 360px;
+  display: flex;
+  flex-direction: column;
+
+  .top {
+    height: 40px;
+  }
+
+  iframe {
+    border: none;
+    width: 100%;
+    flex: auto;
+  }
+
+  > div {
+    flex: none;
+  }
+}
+</style>

+ 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;
+    });
+}

Разница между файлами не показана из-за своего большого размера
+ 1 - 0
src/libs/print/SCPPrint-2.7.js


+ 39 - 0
src/tools/object.js

@@ -0,0 +1,39 @@
+export function transformFlatObjectToNested(model, separator = '_', defaultValue = null) {
+  return Object.entries(model).reduce((model, [key, value]) => {
+    const keys = key.split(separator).reverse();
+    let current = model;
+    while (keys.length) {
+      const k = keys.pop();
+      current = current[k] = keys.length ? (current[k] || {}) : (value || defaultValue);
+    }
+    return model;
+  }, {})
+}
+
+export function transformNestedObjectToFlat(model, separator = '_') {
+  const result = {};
+  const stack = [{obj: model, parentKey: ''}];
+
+  while (stack.length > 0) {
+    const {obj, parentKey} = stack.pop();
+    for (const [key, value] of Object.entries(obj)) {
+      const k = parentKey ? `${parentKey}${separator}${key}` : key;
+      if (value && typeof value === 'object' && !Array.isArray(value)) {
+        stack.push({obj: value, parentKey: k});
+      } else {
+        result[k] = value;
+      }
+    }
+  }
+
+  return result;
+}
+
+export function withResolvers() {
+  let resolve, reject;
+  const promise = new Promise((res, rej) => {
+    resolve = res;
+    reject = rej;
+  });
+  return {promise, resolve, reject};
+}

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

@@ -0,0 +1,129 @@
+/**
+ * [顺丰] 顺丰面单打印实例
+ * @typedef {Object} sf_express_instance
+ * @property {string} version - 实例的版本号。
+ * @property {sf_express_instance_print_function} print - 打印信息的方法。
+ * @property {sf_express_instance_devices_function} devices - 获取打印机设备的方法。
+ */
+
+/**
+ * [顺丰] 打印信息的方法的类型定义。
+ * @callback sf_express_instance_print_function
+ * @param {object} data - 要打印的数据。
+ * @param {Object} [options] - 打印选项(可选)。
+ * @param {'PRINT' | 'PREVIEW'} [options.lodopFn="PRINT"] - 打印类型。
+ * @param {boolean} [options.allPreview=false] - 预览全部面单
+ * @returns {Promise<void>}
+ */
+
+/**
+ * [顺丰] 打印设备的方法的类型定义。
+ * @callback sf_express_instance_devices_function
+ * @param {string} [name] - 打印机名称前缀。
+ * @returns {Promise<{name: string; index: number}[]>}
+ */
+
+
+import {withResolvers} from '@/tools/object';
+
+/**
+ * 获取顺丰打印实例
+ * @param options
+ * @param {string?} options.appid 合作伙伴编码(即顾客编码)
+ * @param {string?} options.appname 打印机设备名称(即设备名称)
+ * @param {'pro' | 'sbox'?} options.env 环境
+ * @returns {Promise<sf_express_instance>}
+ */
+let sf_express = async function (options = {}) {
+  const module = await import('@/libs/print/SCPPrint-2.7.js');
+
+  const {promise: load, resolve, reject} = withResolvers();
+
+  const instance = new module.default({
+    partnerID: options.appid || process.env.VUE_APP_SF_EXPRESS_APPID,
+    env: options.env || process.env.VUE_APP_SF_EXPRESS_ENV || (process.env.NODE_ENV === 'development' ? 'sbox' : 'pro'),
+    notips: false,
+    callback(result) {
+      if (result.code === 1) resolve(instance);
+      else reject({code: result.code, message: result.msg});
+    },
+  });
+
+  try {
+    const instance = await load;
+    sf_express = () => Promise.resolve(instance);
+  } catch (e) {}
+  return instance;
+};
+
+/**
+ *
+ * @type {sf_express_instance_print_function}
+ */
+export async function sf_express_print(data, options = {}) {
+  const instance = await sf_express(options);
+  const {promise: loadDevices, resolve, reject} = withResolvers();
+
+  instance['getPrinters']((result) => {
+    if (result.code === 1) {
+      const devices = Array.isArray(result.printers) ? result.printers : [];
+      resolve(devices);
+    } else reject({message: result.msg, code: result.code});
+  });
+
+  const devices = await loadDevices;
+
+  const device = devices.find(device => device.name.startsWith(process.env.VUE_APP_SF_EXPRESS_APPNAME));
+
+  if (device) instance['setPrinter'](device.index);
+  await instance.print(data);
+}
+
+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) {

+ 509 - 0
src/views/rescription/prescriptionCore/edit.vue

@@ -0,0 +1,509 @@
+<script>
+import dayjs from 'dayjs';
+import {listMedicalMechanism} from '@/api/medical/mechanism';
+import {transformFlatObjectToNested} from '@/tools/object';
+import {bignumber, chain} from 'mathjs';
+import {addPrescriptionCore2} from '@/api/prescription/prescriptionCore';
+import {mapGetters} from 'vuex';
+
+import regionOptions from '@/utils/options';
+
+
+export default {
+  data() {
+    const locationValidator = (message) => (rule, value, callback) => {
+      if (this.model.expressExecutor === '顺丰') {
+        if (!value.length) { return callback(new Error(message)); }
+      }
+      callback();
+    };
+    return {
+      medicines: [],
+      model: {},
+      rules: {
+        yljgId: [{required: true, message: '请选择医疗机构'}],
+        yfId: [{required: true, message: '请选择药房'}],
+        preNo: [{required: true, message: '请输入医院处方号'}],
+        name: [{required: true, message: '请输入患者姓名'}],
+        sex: [{required: true, message: '请选择性别'}],
+        age: [{required: true, message: '请输入年龄'}],
+        // recipientTel: [{required: true, message: '请输入联系电话'}],
+        expressExecutor: [{required: true, message: '请选择配送方式'}],
+        consignee: [{validator: locationValidator('请输入收件电话')}],
+        contactNumber: [{validator: locationValidator('请输入收件电话')}],
+        location: [{validator: locationValidator('请选择所属地域')}],
+        address: [{validator: locationValidator('请输入详细地址')}],
+      },
+
+      ageOptions: [
+        {label: '岁', value: '岁'},
+        {label: '月', value: '月'},
+        {label: '周', value: '周'},
+        {label: '天', value: '天'},
+      ],
+      genderOptions: [
+        {label: '男', value: '男'},
+        {label: '女', value: '女'},
+      ],
+      recipeCategoryOptions: [
+        {label: '门诊', value: '1'},
+        {label: '住院', value: '2'},
+      ],
+      expressExecutorOptions: [
+        {label: '送医院', value: '送医院'},
+        {label: '顺丰', value: '顺丰'},
+        {label: '其他', value: '其他'},
+      ],
+      whetherOptions: [
+        {label: '是', value: '1'},
+        {label: '否', value: '0'},
+      ],
+      communityOptions: [],
+      mechanismOptions: [],
+      dosageFormOptions: [],
+      usageOptions: [],
+      concentrationOptions: [],
+      frequencyOptions: [],
+      medicationTimeOptions: [],
+      regionOptions,
+      showPharmacy: false,
+      searchTableForSystemColumns: [
+        {title: '名称', data: 'drugsName'},
+        {title: '编码', data: 'drugsId'},
+        {title: '规格', data: 'drugsSpecsName'},
+        {title: '产地', data: 'placeName'},
+        {title: '零售价', data: 'retail'},
+      ],
+      saving: false,
+    };
+  },
+  computed: {
+    ...mapGetters(['pharmacyList']),
+    getEmptyMedicineRowIndex() {
+      return this.medicines.findIndex(medicine => Object.keys(medicine).length === 0);
+    },
+  },
+  created() {
+    this.formReset();
+    this.getDicts('dosage_form').then((response) => {this.dosageFormOptions = response.data;});
+    this.getDicts('prescription_usage').then((response) => {this.usageOptions = response.data;});
+    this.getDicts('concentration').then((response) => {this.concentrationOptions = response.data;});
+    this.getDicts('frequency').then((response) => {this.frequencyOptions = response.data;});
+    this.getDicts('medicationTime').then((response) => {this.medicationTimeOptions = response.data;});
+  },
+  methods: {
+    formReset() {
+      const pharmacies = Array.isArray(this.pharmacyList) ? this.pharmacyList : [];
+      this.model = {
+        yfId: window.localStorage.getItem('pharmacyId') || (pharmacies.length ? pharmacies[0].id : ''),
+        /* 01 受理时间 */ tackleTime: dayjs().startOf('day').format('YYYY-MM-DD'),
+        /* 02 处方序号 */ recipeSerial: '',
+        /* 03 处方编号 */ recipeBh: '',
+        /* 04 患者姓名 */ name: '',
+        /* 05 患者年龄 */ // age: '',
+        /* 07 患者性别 */ sex: '',
+        /* 08 联系电话 */ recipientTel: '',
+        /* 09 医院处方 */ preNo: '',
+        /* 10 医疗机构 */ yljgId: '',
+        /* 11 门诊住院 */ preMzZy: '',
+        /* 12 就诊科室 */ department: '',
+        /* 13 病区     */ inpatientArea: '',
+        /* 14 病床     */ bedNo: '',
+        /* 15 就诊医生 */ doctor: '',
+        /* 16 临床诊断 */ disName: '',
+        /* 17 剂数     */ // prescription_number: '',
+        /* 18 剂型     */ prescription_dosageForm: '',
+        /* 19 处方用法 */ prescription_prescriptionUsage: '',
+        /* 20 浓煎量   */ prescription_concentration: '',
+        /* 21 服药频次 */ prescription_frequency: '',
+        /* 22 服药时间 */ prescription_medicationTime: '',
+        /* 23 医嘱     */ prescription_remark: '',
+        /* 24 代煎     */ isBehalf: '',
+        /* 26 配送方式 */ expressExecutor: '',
+        /* 27 收件人名 */ consignee: '',
+        /* 28 收件电话 */ contactNumber: '',
+        location: [],
+        /* 29 收件地址 */ address: '',
+        /* 30 快递单号 */ expressCode: '',
+        /* 31 处方金额 */ // prescription_prescriptionSum: '',
+        /* 32 代煎费用 */ // prescription_daijianCost: '',
+        /* 33 配送费用 */ // prescription_distributionCost: '',
+        /* 34 总金额 */ // prescription_prescriptionTotleSum: '',
+      };
+      this.getMechanismList();
+      this.appendMedicine();
+
+      this.showPharmacy = pharmacies.length > 1 || !this.model.yfId;
+    },
+    /** 查询医疗机构列表 */
+    async getMechanismList() {
+      try {
+        const {data: rows} = await listMedicalMechanism();
+        this.mechanismOptions = rows.map(item => (item.disabled = item.state === '0', item));
+      } catch (error) {
+        this.mechanismOptions = [];
+        this.model.yljgId = '';
+      }
+    },
+    async handle() {
+      try {
+        await this.$refs.form.validate();
+        const medicines = this.medicines.filter(medicine => Object.keys(medicine).length);
+        if (medicines.length === 0) throw '请至少添加一味药品';
+        if (medicines.find(medicine => !medicine.matDose)) throw '请补全药品剂量';
+
+        const model = transformFlatObjectToNested(this.model);
+        model['prescription'].drugs = medicines;
+        [model.province = '', model.city = '', model.region = ''] = this.model.location;
+        this.saving = true;
+        await addPrescriptionCore2(model);
+        this.$emit('close', true);
+      } catch (e) {
+        if (e === false) this.$message.warning('请补全表单数据');
+        else if (typeof e === 'string') this.$message.warning(e);
+      }
+      this.saving = false;
+    },
+    cancel() {
+      this.$emit('close', false);
+    },
+    updateValidator(validators) {
+      this.$refs.form.clearValidate(validators);
+      this.$refs.form.validateField(validators);
+    },
+
+    searchMedicinesFormat(res) {
+      try {
+        if (res.rows) return {totalRow: res.total, list: res.rows};
+        else return {totalRow: res.data.length, list: res.data};
+      } catch (error) { return {totalRow: 0, list: []}; }
+    },
+    selectMedicine([value], row, index) {
+      if (value) {
+        row.matCode = value.drugsId;
+        row.matName = value.drugsName;
+        row.drugsSpecsName = value.drugsSpecsName;
+        row.matUsageName = value.matUsageName;
+        // row.matDose = '';
+        row.matUnitName = value.dosageSizeUnit;
+        row.matXsj = value.retail;
+        row.subtotalMoney = this.calculationMedicinePrice(row);
+      } else {
+        delete row.matCode;
+        delete row.matName;
+        delete row.drugsSpecsName;
+        delete row.matUsageName;
+        delete row.matDose;
+        delete row.matUnitName;
+        delete row.matXsj;
+        this.medicines.splice(index, 1, row);
+      }
+      if (this.getEmptyMedicineRowIndex === -1) this.appendMedicine({}, index + 1);
+    },
+    appendMedicine(model, index = this.medicines.length) {
+      this.medicines.splice(index, 0, model || {});
+    },
+    deleteMedicine(row, index) {
+      this.medicines.splice(index, 1);
+    },
+    updateMedicine(row, index) {
+      this.medicines.splice(index, 1, {...row, subtotalMoney: this.calculationMedicinePrice(row)});
+    },
+    calculationMedicinePrice(medicine) {
+      let price = chain(bignumber(medicine.matXsj || 0));
+      return price.multiply(bignumber(medicine.matDose || 0)).value.valueOf();
+    },
+  },
+};
+</script>
+
+<template>
+  <div>
+    <el-form ref="form" :model="model" :rules="rules" label-width="100px">
+      <el-row :gutter="20">
+        <el-col :span="6">
+          <el-form-item label="受理时间" prop="tackleTime">
+            <el-date-picker v-model="model.tackleTime" type="date" value-format="yyyy-MM-dd"
+                            placeholder="请选择受理时间"></el-date-picker>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="序号" prop="recipeSerial">
+            <el-input v-model="model.recipeSerial" placeholder="请输入序号"/>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="编号" prop="recipeBh">
+            <el-input v-model="model.recipeBh" placeholder="请输入编号"/>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="6">
+          <el-form-item label="患者" prop="name">
+            <el-input v-model="model.name" placeholder="请输入患者姓名"/>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="年龄" prop="age">
+            <div style="display: flex">
+              <el-input-number v-model="model.age" :min="0" label="请输入年龄"></el-input-number>
+            </div>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="性别" prop="sex">
+            <el-select v-model="model.sex" placeholder="请选择性别" clearable>
+              <el-option v-for="item in genderOptions" :key="item.label" :label="item.label"
+                         :value="item.value"></el-option>
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="联系电话" prop="recipientTel">
+            <el-input type="tel" v-model="model.recipientTel" placeholder="请输入联系电话" minlength="11"
+                      maxlength="11"/>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="6">
+          <el-form-item label="医院处方号" prop="preNo">
+            <el-input v-model="model.preNo" placeholder="请输入医院处方号"/>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="医疗机构" prop="yljgId">
+            <el-select v-model="model.yljgId" placeholder="请选择医疗机构" clearable>
+              <el-option v-for="item in mechanismOptions" :key="item.code" :value="item.code" :label="item.name"
+                         :disabled="item.disabled"></el-option>
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="药房" prop="yfId">
+            <el-select v-model="model.yfId" placeholder="请选择药房" clearable>
+              <el-option v-for="item in pharmacyList" :key="dict.id" :label="dict.name" :value="dict.id"
+                         :disabled="item.disabled"></el-option>
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="门诊住院" prop="preMzZy">
+            <el-select v-model="model.preMzZy" placeholder="请选择" clearable>
+              <el-option v-for="item in recipeCategoryOptions" :key="item.label" :label="item.label"
+                         :value="item.value"></el-option>
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="就诊科室" prop="department">
+            <el-input v-model="model.department" placeholder="请输入就诊科室"/>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="病区" prop="inpatientArea">
+            <el-input v-model="model.inpatientArea" placeholder="请输入病区"/>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="病床" prop="bedNo">
+            <el-input v-model="model.bedNo" placeholder="请输入病床"/>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="就诊医生" prop="doctor">
+            <el-input v-model="model.doctor" placeholder="请输入就诊医生"/>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="临床诊断" prop="disName">
+            <el-input v-model="model.disName" placeholder="请输入临床诊断"/>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="剂数" prop="prescription_number">
+            <el-input-number v-model="model.prescription_number" :min="0" label="请输入剂数"></el-input-number>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="剂型" prop="prescription_dosageForm">
+            <el-select v-model="model.prescription_dosageForm" placeholder="请选择剂型" clearable>
+              <el-option v-for="item in dosageFormOptions" :key="item.dictValue"
+                         :label="item.dictLabel" :value="item.dictValue"
+              ></el-option>
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="处方用法" prop="prescription_prescriptionUsage">
+            <el-select v-model="model.prescription_prescriptionUsage" placeholder="请选择处方用法" clearable>
+              <el-option v-for="item in usageOptions" :key="item.dictValue"
+                         :label="item.dictLabel" :value="item.dictValue"
+              ></el-option>
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="浓煎量" prop="prescription_concentration">
+            <el-select v-model="model.prescription_concentration" placeholder="请选择浓煎量" clearable>
+              <el-option v-for="item in concentrationOptions" :key="item.dictValue"
+                         :label="item.dictLabel" :value="item.dictValue"
+              ></el-option>
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="服药频次" prop="prescription_frequency">
+            <el-select v-model="model.prescription_frequency" placeholder="请选择服药频次" clearable>
+              <el-option v-for="item in frequencyOptions" :key="item.dictValue"
+                         :label="item.dictLabel" :value="item.dictValue"
+              ></el-option>
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="服药时间" prop="prescription_medicationTime">
+            <el-select v-model="model.prescription_medicationTime" placeholder="请选择服药时间" clearable>
+              <el-option v-for="item in medicationTimeOptions" :key="item.dictValue"
+                         :label="item.dictLabel" :value="item.dictValue"
+              ></el-option>
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="是否代煎" prop="isBehalf">
+            <el-radio-group v-model="model.isBehalf">
+              <el-radio v-for="item in whetherOptions" :key="item.value" :label="item.value">{{ item.label }}</el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </el-col>
+        <el-col :span="24">
+          <el-form-item label="医嘱" prop="prescription_remark">
+            <el-input v-model="model.prescription_remark" placeholder="请输入医嘱"/>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="6">
+          <el-form-item label="配送方式" prop="expressExecutor">
+            <el-select v-model="model.expressExecutor" placeholder="请选择配送方式" clearable
+                       @change="updateValidator(['consignee', 'contactNumber', 'location', 'address'])">
+              <el-option v-for="item in expressExecutorOptions" :key="item.label" :label="item.label"
+                         :value="item.value"></el-option>
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="收件人" prop="consignee">
+            <el-input v-model="model.consignee" placeholder="请输入收件人"/>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="收件电话" prop="contactNumber">
+            <el-input type="tel" v-model="model.contactNumber" placeholder="请输入收件电话" minlength="11"
+                      maxlength="11"/>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="所属地域" prop="location">
+            <el-cascader v-model="model.location" :options="regionOptions" :props="{ value: 'label' }"
+                         placeholder="请选择省/市/区" clearable></el-cascader>
+          </el-form-item>
+        </el-col>
+        <el-col :span="18">
+          <el-form-item label="收件地址" prop="address">
+            <el-input v-model="model.address" placeholder="请输入详细地址"/>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="快递单号" prop="expressCode">
+            <el-input v-model="model.expressCode" placeholder="请输入快递单号"/>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="6">
+          <el-form-item label="处方金额" prop="prescription_prescriptionSum">
+            <el-input-number v-model="model.prescription_prescriptionSum" :precision="2" :step="1"
+                             :min="0"></el-input-number>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="代煎费用" prop="prescription_daijianCost">
+            <el-input-number v-model="model.prescription_daijianCost" :precision="2" :step="1"
+                             :min="0"></el-input-number>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="配送费用" prop="prescription_distributionCost">
+            <el-input-number v-model="model.prescription_distributionCost" :precision="2" :step="1"
+                             :min="0"></el-input-number>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="总金额" prop="prescription_prescriptionTotleSum">
+            <el-input-number v-model="model.prescription_prescriptionTotleSum" :precision="2" :step="1"
+                             :min="0"></el-input-number>
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    <el-table :data="medicines" style="width: 100%">
+      <el-table-column label="序号" type="index" width="65" align="center"></el-table-column>
+      <el-table-column label="药品名称" prop="matName" align="center">
+        <template slot-scope="scope">
+          <template v-if="!scope.row.matCode">
+            <v-selectpage :tb-columns="searchTableForSystemColumns"
+                          key-field="drugsId" v-model="scope.row.matCode"
+                          placeholder="请输入药品名称" title="药品"
+                          data="drugsSearchForSystem" :params="{}" :pagination="false"
+                          :result-format="searchMedicinesFormat"
+                          @values="selectMedicine($event, scope.row, scope.$index)"
+            ></v-selectpage>
+          </template>
+          <a v-else>
+            {{ scope.row.matName }}
+            <i class="icon el-icon-circle-close" @click="selectMedicine([], scope.row, scope.$index)"></i>
+          </a>
+        </template>
+      </el-table-column>
+      <el-table-column label="药品规格" prop="drugsSpecsName" align="center"></el-table-column>
+      <el-table-column label="剂量单位" prop="matUnitName" width="110" align="center"></el-table-column>
+      <el-table-column label="药品用法" prop="matUsageName" align="center">
+        <template slot-scope="scope" v-if="scope.row.matCode">
+          <el-input v-model="scope.row.matUsageName" :disabled="saving"
+                    @input="updateMedicine(scope.row, scope.$index)"></el-input>
+        </template>
+      </el-table-column>
+      <el-table-column label="剂量" prop="matDose" align="center">
+        <template slot-scope="scope" v-if="scope.row.matCode">
+          <el-input-number v-model="scope.row.matDose" :min="0" :precision="2" :controls="false"
+                           :disabled="saving"
+                           @change="updateMedicine(scope.row, scope.$index)"></el-input-number>
+        </template>
+      </el-table-column>
+      <el-table-column label="药品单价" prop="matXsj" width="110" align="center"></el-table-column>
+      <el-table-column label="小计" prop="subtotalMoney" width="110" align="center"></el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="80" fixed="right">
+        <template slot-scope="scope">
+          <el-button style="width: 40px;" type="danger" size="mini"
+                     :disabled="Object.keys(scope.row).length === 0 || saving"
+                     @click="deleteMedicine(scope.row, scope.$index)"
+          >删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <div class="el-dialog__footer">
+      <el-button type="primary" :loading="saving" @click="handle">保 存</el-button>
+      <el-button @click="cancel">取 消</el-button>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.el-input, .el-select, .el-cascader {
+  width: 100%;
+}
+</style>

+ 22 - 5
src/views/rescription/prescriptionCore/index.vue

@@ -141,9 +141,8 @@
               @click="handleQuery"
               >搜索</el-button
             >
-            <el-button icon="el-icon-refresh" size="mini" @click="resetQuery"
-              >重置</el-button
-            >
+            <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+            <el-button icon="el-icon-circle-plus-outline" v-hasPermi="['rescription:prescriptionCore:add']" size="mini" @click="recipeEditOpen = true;">新增处方</el-button>
           </el-form-item>
         </div>
       </div>
@@ -256,7 +255,7 @@
             size="mini"
             class="printbtn"
             style="width: 40px"
-            @click="consoleBtn(scope.row)"
+            @click="showPrintContentMethod(scope.row)"
             v-if="scope.row.checkState == 1"
             >打印</el-button
           >
@@ -1303,6 +1302,14 @@
         <el-button type="primary" @click="setRemark">确 定</el-button>
       </div>
     </el-dialog>
+
+    <el-dialog :fullscreen="true" title="新增处方" :visible.sync="recipeEditOpen" width="1180px" append-to-body size="mini" destroy-on-close>
+      <edit @close="recipeEditOpen = false;$event && getList()"></edit>
+    </el-dialog>
+
+    <el-dialog title="打印预览" :visible.sync="showPrint" @closed="showPrintId = ''">
+      <print-container v-if="showPrintId" :id="showPrintId"></print-container>
+    </el-dialog>
   </div>
 </template>
 
@@ -1337,7 +1344,9 @@ import {
 import { updatePrintStatus } from "@/api/prescription/prescriptionCore";
 import Pres from "@/components/Pres/index.vue";
 import JsBarcode from "@/components/JsBarcode/index.vue";
+import Edit from "./edit.vue";
 import dayjs from "dayjs";
+import PrintContainer from '@/views/rescription/prescriptionCore/printContainer.vue';
 
 // import JsBarcode from "jsbarcode"
 
@@ -1358,10 +1367,12 @@ function debounce(fn, delay) {
 
 export default {
   name: "PrescriptionCore",
-  components: { Pres, JsBarcode },
+  components: {PrintContainer, Pres, JsBarcode, Edit },
   data() {
     const now = dayjs().format("YYYY-MM-DD");
     return {
+      recipeEditOpen: false,
+
       curPreNo: "",
       curName: "",
       curSex: "",
@@ -1464,6 +1475,8 @@ export default {
       open: false,
       // print相关
       openPrint: false,
+      showPrint: false, // 打印页面弹窗
+      showPrintId: '', // 打印页面弹窗
       activePrint: "标签",
       printTypeList: [
         { name: "标签", id: 0 },
@@ -1823,6 +1836,10 @@ export default {
       this.dialogFormVisible = false;
     },
     //打印
+    showPrintContentMethod(row) {
+      this.showPrint = true;
+      this.showPrintId = row.id;
+    },
     consoleBtn(row) {
       this.reset();
       this.activePrint = "标签";

+ 82 - 0
src/views/rescription/prescriptionCore/printContainer.vue

@@ -0,0 +1,82 @@
+<script>
+import Print_tag_80_50 from '@/components/print/tag_80_50.vue';
+import Print_recipe_a5 from '@/components/print/recipe_A5.vue';
+import Print_ticket_72 from '@/components/print/ticket_72.vue';
+import Print_express_75 from '@/components/print/express_75.vue';
+import {updatePrintStatus} from '@/api/prescription/prescriptionCore';
+
+export default {
+  name: 'printContainer',
+  props: {
+    id: {type: [String, Number], required: true},
+  },
+  components: {Print_express_75, Print_ticket_72, Print_recipe_a5, Print_tag_80_50},
+  data() {
+    return {
+      selected: 0,
+    };
+  },
+  computed: {
+    height() { return `${window.innerHeight * 0.8}`; },
+    style() { return {height: `${this.height}px`}; },
+  },
+  watch: {
+    selected(index) {
+      try {
+        const el = this.$refs[`print${index}`];
+        el['print'](true);
+      } catch (e) {}
+    },
+  },
+  methods: {
+    async print() {
+      const keys = Object.keys(this.$refs).filter(key => key.startsWith('print'));
+      try {
+        for (const key of keys) {
+          const el = this.$refs[key];
+          await el['print']();
+        }
+        this.$message.success(`组合打印成功`);
+      } catch (e) {
+        this.$message.error(`出错了,请重试`);
+      }
+    },
+    update(type) {
+      if (type === 0 || type === 1) {
+        updatePrintStatus({
+          isPrint: '1',
+          id: this.id,
+        });
+      }
+    },
+  },
+};
+</script>
+
+<template>
+  <div>
+    <el-button class="pin" type="primary" @click="print">组合打印</el-button>
+    <el-tabs tab-position="left" :style="style" v-model="selected">
+      <el-tab-pane label="标签" name="0">
+        <print_tag_80_50 ref="print0" :id="id" @click="$event && update(0)"></print_tag_80_50>
+      </el-tab-pane>
+      <el-tab-pane label="处方笺" name="1">
+        <print_recipe_a5 ref="print1" :id="id" :style="style" @click="$event && update(1)"></print_recipe_a5>
+      </el-tab-pane>
+      <el-tab-pane label="药品清单" name="2">
+        <print_ticket_72 ref="print2" :id="id" :style="style" @click="$event && update(2)"></print_ticket_72>
+      </el-tab-pane>
+      <el-tab-pane label="快递单" name="3">
+        <print_express_75 ref="print3" :id="id" :style="style" @click="$event && update()"></print_express_75>
+      </el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.pin {
+  position: absolute;
+  top: 12px;
+  right: 60px;
+}
+</style>

Некоторые файлы не были показаны из-за большого количества измененных файлов