VuePressVuePress
首页
  • 基础
  • UI
  • JavaScript
  • CSS
  • postcss
  • Vue3
  • Vue的设计与实现
  • 前端常用插件
  • PHP
  • Laravel
  • Linux
  • 线性代数
Category
AI
jiyun
Timeline
首页
  • 基础
  • UI
  • JavaScript
  • CSS
  • postcss
  • Vue3
  • Vue的设计与实现
  • 前端常用插件
  • PHP
  • Laravel
  • Linux
  • 线性代数
Category
AI
jiyun
Timeline
  • 基础知识

    • ref&reactive
    • watchEffect
    • 响应式: 工具函数
    • 组合式函数
    • 防止ref不会自动解包
    • 注入全局属性
  • 虚拟节点

    • 虚拟节点介绍
    • component 属性
    • component.proxy
    • appContext
  • 组件

    • 函数式组件
  • 过渡和动画

    • 函数式组件
  • vite

    • import.meta.glob
    • import.meta.url
  • demo

    • 打印面单-雏形
      • 1. 初始化项目
      • 2. 完整代码
        • 2.1 main.js
        • 2.2 App.vue
        • 2.3 ExpressLabelForm.vue
        • 2.4 ExpressLabelPreview.vue
        • 2.5 ExpressLabelPrinter.vue
        • 2.6 fakeDataGenerator.js
    • 打印面单-多模板

快递面单打印雏形

1. 初始化项目

# 初始化项目
npm create vue@latest ./

# 安装依赖 https://lindell.me/JsBarcode/
npm install jsbarcode

# npm install -D sass-embedded

2. 完整代码

2.1 main.js

src/main.js

import './assets/main.css'

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

2.2 App.vue

src/App.vue

<template>
  <div id="app">
    <ExpressLabelPrinter />
  </div>
</template>

<script setup>
import ExpressLabelPrinter from './components/ExpressLabelPrinter.vue'
</script>

<style>
#app {
  font-family: Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}

@media print {
  body {
    margin: 0;
    padding: 0;
  }

  #app {
    margin-top: 0;
  }

  @page {
    margin: 0;
  }
}
</style>

2.3 ExpressLabelForm.vue

src/components/ExpressLabelForm.vue

<template>
    <form @submit.prevent="submitForm">
      <div>
        <label for="sender">寄件人:</label>
        <input id="sender" v-model="sender" required>
      </div>
      <div>
        <label for="receiver">收件人:</label>
        <input id="receiver" v-model="receiver" required>
      </div>
      <div>
        <label for="address">地址:</label>
        <input id="address" v-model="address" required>
      </div>
      <div>
        <label for="phone">电话:</label>
        <input id="phone" v-model="phone" required>
      </div>
      <button type="submit">预览面单</button>
    </form>
  </template>
  
  <script setup>
  import { ref, onMounted } from 'vue'
  import { generateRandomTestData } from '../utils/fakeDataGenerator'
  
  const sender = ref('')
  const receiver = ref('')
  const address = ref('')
  const phone = ref('')
  
  const emit = defineEmits(['previewLabel'])
  
  const submitForm = () => {
    emit('previewLabel', {
      sender: sender.value,
      receiver: receiver.value,
      address: address.value,
      phone: phone.value
    })
  }
  
  // 填充随机测试数据
  const fillRandomTestData = () => {
    const testData = generateRandomTestData()
    sender.value = testData.sender
    receiver.value = testData.receiver
    address.value = testData.address
    phone.value = testData.phone
  }
  
  // 组件挂载时填充随机测试数据
  onMounted(() => {
    fillRandomTestData()
  })
  </script>
  
  <style scoped>
  form div {
    margin-bottom: 10px;
  }
  label {
    display: inline-block;
    width: 80px;
  }
  input {
    width: 200px;
  }
  </style>

2.4 ExpressLabelPreview.vue

src/components/ExpressLabelPreview.vue

<template>
    <div>
      <div class="preview" :style="previewStyle">
        <h2>快递面单</h2>
        <table>
          <tbody>
            <tr>
            <td><strong>寄件人:</strong></td>
            <td>{{ labelData.sender }}</td>
          </tr>
          <tr>
            <td><strong>收件人:</strong></td>
            <td>{{ labelData.receiver }}</td>
          </tr>
          <tr>
            <td><strong>地址:</strong></td>
            <td>{{ labelData.address }}</td>
          </tr>
          <tr>
            <td><strong>电话:</strong></td>
            <td>{{ labelData.phone }}</td>
          </tr>
          </tbody>
        </table>
        <div class="barcode-container">
          <svg id="barcode"></svg>
        </div>
      </div>
      <div class="no-print">
        <label for="print-quantity">打印数量:</label>
        <input id="print-quantity" type="number" v-model.number="printQuantity" min="1" step="1">
        <label for="start-number">起始序号:</label>
        <input id="start-number" type="number" v-model.number="startNumber" min="1" step="1">
        <button @click="print">打印面单</button>
      </div>
    </div>
  </template>
  
  <script setup>
import { ref, computed, onMounted, watch } from 'vue'
import JsBarcode from 'jsbarcode'

const props = defineProps({
  labelData: {
    type: Object,
    required: true
  },
  labelType: {
    type: String,
    default: 'standard'
  },
  customWidth: {
    type: Number,
    default: 100
  },
  customHeight: {
    type: Number,
    default: 150
  }
})

const emit = defineEmits(['print'])

const printQuantity = ref(1)
const startNumber = ref(1)

const print = () => {
  emit('print', { quantity: printQuantity.value, startNumber: startNumber.value })
}

const previewStyle = computed(() => {
  let width, height
  switch (props.labelType) {
    case 'standard':
      width = 100
      height = 150
      break
    case 'thermal':
      width = 100
      height = 180
      break
    case 'custom':
      width = props.customWidth
      height = props.customHeight
      break
    default:
      width = 100
      height = 150
  }
  return {
    width: `${width}mm`,
    height: `${height}mm`,
    border: '1px solid #ccc',
    padding: '10px',
    fontSize: '12px',
    boxSizing: 'border-box',
    marginBottom: '10px'
  }
})

const generateBarcode = () => {
  const barcodeValue = props.labelData.barcode || `${props.labelData.phone}${Date.now().toString().slice(-6)}`
  JsBarcode("#barcode", barcodeValue, {
    format: "CODE128",
    width: 2,
    height: 50,
    displayValue: true
  })
}

onMounted(() => {
  generateBarcode()
})

watch(() => props.labelData, () => {
  generateBarcode()
}, { deep: true })
  </script>
  
  <style scoped>
  h2 {
    text-align: center;
    margin-bottom: 10px;
    font-size: 14px;
  }
  
  table {
    width: 100%;
    border-collapse: collapse;
  }
  
  td {
    padding: 5px;
    border: 1px solid #ccc;
  }
  
  .no-print {
    margin-top: 10px;
  }
  
  input[type="number"] {
    width: 50px;
    margin-right: 10px;
  }
  
  @media print {
    .preview {
      border: none !important;
      padding: 0 !important;
    }
  
    table {
      border: 1px solid #000;
    }
  
    td {
      border: 1px solid #000;
    }
  
    .no-print {
      display: none;
    }
  }

  input[type="number"] {
  width: 50px;
  margin-right: 10px;
}

.barcode-container {
  margin-top: 10px;
  text-align: center;
}
  </style>

2.5 ExpressLabelPrinter.vue

src/components/ExpressLabelPrinter.vue

<template>
  <div class="express-label">
    <div class="no-print">
      <h1>快递面单打印</h1>
      <ExpressLabelForm @preview-label="previewLabel" />
      <div>
        <label for="label-type">面单类型:</label>
        <select id="label-type" v-model="labelType">
          <option value="standard">标准快递面单 (100mm x 150mm)</option>
          <option value="thermal">热敏快递面单 (100mm x 180mm)</option>
          <option value="custom">自定义尺寸</option>
        </select>
      </div>
      <div v-if="labelType === 'custom'">
        <label for="custom-width">宽度 (mm):</label>
        <input id="custom-width" type="number" v-model="customWidth" min="1" step="1">
        <label for="custom-height">高度 (mm):</label>
        <input id="custom-height" type="number" v-model="customHeight" min="1" step="1">
      </div>
      <!-- 添加测试模式开关 -->
      <div>
        <label for="test-mode">测试模式:</label>
        <input id="test-mode" type="checkbox" v-model="testMode">
      </div>
    </div>
    <ExpressLabelPreview
      v-if="showPreview"
      :labelData="labelData"
      :labelType="labelType"
      :customWidth="customWidth"
      :customHeight="customHeight"
      @print="printLabel"
    />
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
import ExpressLabelForm from './ExpressLabelForm.vue'
import ExpressLabelPreview from './ExpressLabelPreview.vue'
import JsBarcode from 'jsbarcode'

const showPreview = ref(false)
const labelData = ref({})
const labelType = ref('standard')
const customWidth = ref(100)
const customHeight = ref(150)
const testMode = ref(true) // 默认开启测试模式

const labelSize = computed(() => {
  switch (labelType.value) {
    case 'standard':
      return { width: 100, height: 150 }
    case 'thermal':
      return { width: 100, height: 180 }
    case 'custom':
      return { width: customWidth.value, height: customHeight.value }
    default:
      return { width: 100, height: 150 }
  }
})

const previewLabel = (data) => {
  labelData.value = data
  showPreview.value = true
}

const printLabel = ({ quantity, startNumber }) => {
  if (testMode.value) {
    // 测试模式:打开新窗口
    openPrintWindow({ quantity, startNumber })
  } else {
    // 非测试模式:直接打印
    printDirectly({ quantity, startNumber })
  }
}

const openPrintWindow = ({ quantity, startNumber }) => {
  const printWindow = window.open('', '_blank')

  printWindow.document.write('<html><head><title>打印快递面单</title>')
  printWindow.document.write('<style>')
  printWindow.document.write(`
    @page {
      size: ${labelSize.value.width}mm ${labelSize.value.height}mm;
      margin: 0;
    }
    body {
      margin: 0;
      padding: 0;
    }
    .label-container {
      width: ${labelSize.value.width}mm;
      height: ${labelSize.value.height}mm;
      page-break-after: always;
      padding: 5mm;
      box-sizing: border-box;
      position: relative;
    }
    .label-number {
      position: absolute;
      top: 2mm;
      right: 2mm;
      font-size: 12px;
      font-weight: bold;
    }
    table {
      width: 100%;
      border-collapse: collapse;
    }
    td {
      border: 1px solid black;
      padding: 2mm;
    }
    .barcode-container {
      margin-top: 10px;
      text-align: center;
    }
  `)
  printWindow.document.write('</style></head><body>')

  const labelContent = document.querySelector('.preview').innerHTML

  const totalLabels = quantity // 总标签数量

  for (let i = 0; i < quantity; i++) {
    const currentNumber = startNumber + i
    printWindow.document.write(`
      <div class="label-container">
        <div class="label-number">第 ${currentNumber} 张,共 ${totalLabels} 张</div>
        ${labelContent}
      </div>
    `)
  }

  printWindow.document.write('</body></html>')
  printWindow.document.close()

  printWindow.onload = function() {
    // 为每个标签生成条形码
    const barcodes = printWindow.document.querySelectorAll('#barcode')
    barcodes.forEach((barcode, index) => {
      const barcodeValue = labelData.value.barcode || `${labelData.value.phone}${Date.now().toString().slice(-6)}${index}`
      JsBarcode(barcode, barcodeValue, {
        format: "CODE128",
        width: 2,
        height: 50,
        displayValue: true
      })
    })
    
    setTimeout(() => {
      printWindow.print()
      printWindow.onafterprint = function() {
        printWindow.close()
      }
    }, 500) // 给一些时间让条形码生成
  }
}

const printDirectly = ({ quantity, startNumber }) => {
  // 创建一个隐藏的 iframe 来加载打印内容
  const iframe = document.createElement('iframe')
  iframe.style.display = 'none'
  document.body.appendChild(iframe)

  const doc = iframe.contentWindow.document
  doc.open()
  doc.write('<html><head><title>打印快递面单</title>')
  doc.write('<style>')
  // ... 添加与 openPrintWindow 函数中相同的样式 ...
  doc.write('</style></head><body>')

  const labelContent = document.querySelector('.preview').innerHTML
  const totalLabels = quantity

  for (let i = 0; i < quantity; i++) {
    const currentNumber = startNumber + i
    doc.write(`
      <div class="label-container">
        <div class="label-number">第 ${currentNumber} 张,共 ${totalLabels} 张</div>
        ${labelContent}
      </div>
    `)
  }

  doc.write('</body></html>')
  doc.close()

  // 在 iframe 加载完成后生成条形码并打印
  iframe.onload = () => {
    const barcodes = iframe.contentWindow.document.querySelectorAll('#barcode')
    barcodes.forEach((barcode, index) => {
      const barcodeValue = labelData.value.barcode || `${labelData.value.phone}${Date.now().toString().slice(-6)}${index}`
      JsBarcode(barcode, barcodeValue, {
        format: "CODE128",
        width: 2,
        height: 50,
        displayValue: true
      })
    })
    
    setTimeout(() => {
      iframe.contentWindow.print()
      // 打印完成后移除 iframe
      setTimeout(() => {
        document.body.removeChild(iframe)
      }, 100)
    }, 500) // 给一些时间让条形码生成
  }
}
</script>

<style scoped>
.express-label {
  max-width: 500px;
  margin: 0 auto;
  padding: 20px;
}

@media print {
  .no-print {
    display: none;
  }
  
  .express-label {
    max-width: 100%;
    padding: 0;
  }
}
</style>

2.6 fakeDataGenerator.js

src/utils/fakeDataGenerator.js

// 生成随机测试数据
export function generateRandomTestData() {
const names = ['张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十']
const cities = ['北京市', '上海市', '广州市', '深圳市', '成都市', '杭州市', '武汉市', '南京市']
const districts = ['朝阳区', '浦东新区', '天河区', '南山区', '武侯区', '西湖区', '江汉区', '鼓楼区']
const streets = ['某某路', '某某街', '某某大道', '某某巷']

const sender = names[Math.floor(Math.random() * names.length)]
const receiver = names[Math.floor(Math.random() * names.length)]

const city = cities[Math.floor(Math.random() * cities.length)]
const district = districts[Math.floor(Math.random() * districts.length)]
const street = streets[Math.floor(Math.random() * streets.length)]
const houseNumber = Math.floor(Math.random() * 1000) + 1
const address = `${city}${district}${street}${houseNumber}号`

const phone = `1${Math.floor(Math.random() * 9 + 1)}${Array(9).fill(0).map(() => Math.floor(Math.random() * 10)).join('')}`

return {
sender,
receiver,
address,
phone
}
}
Last Updated:
Contributors: BaronYan
Next
打印面单-多模板