首发于 程序媛的前半生
草稿版Vue-POS机小票模板打印系统[前传]

草稿版Vue-POS机小票模板打印系统[前传]

很久很久很久没来知乎了,如果这是一片大草原,草应该都比我高了吧。最近换了份新工作,离开了外包公司,来到了专门做自己产品的公司。如果问我有何体会,我只想说,钱多好办事,嘻嘻嘻嘻嘻,没办法姐姐我很穷。

这个星期一直在往‘pos机小票模板’配置打印的方向攻克,原本只想当个伸手党,百度了一堆关键词例如:

“自动化配置模板”=>出来了模板读取方式;

“pos小票配置”=>出来了word形式作好模板,复制粘贴加入数据;

“pos小票打印模板”=>出来了如何window打印,不安装任何驱动;

等等等等,但全部都不是我要的,我要的是那种类似于涂鸦板之类的东西,可又不是涂鸦板,因为我不用画图,我的数据全部都是数据源方式,需要客户自己配置自己门店的pos机热敏打印模板,或者pos阶段报表清单等,意思是我想放什么字段就放什么字段,由于pos机需要一些货品信息,打印出来的小票还需要合计之类的。

what the fuck?!明确的跟上级表示臣妾做不到啊,得到了土豪的决定“买买买”。

然后我就开始百度关键词“pos机小票模板购买”,出来的都是卖设备的,不是卖模板的,原本打算随便买个Excel的版本,类似于SpreadJS似乎做得挺不错,可人家客服根本不鸟我,那就抛弃它,原本我也没看上他们家,死贵死贵一年要19万多的体验费用+维护费,滚粗。

后来去淘宝上看到一款还真不错,就是我要的那种,叫做‘康虎打印系统’,马上见到救命恩人的喜悦感,它长下面这样,多好的亲人。

康湖打印系统

都准备好了钱,准备下单了,留了个心眼,去百度下载了绿色官网版本。严重的发现了一个问题,这个系统是后台写的,意思是如果需要用到他们家这个模板,必须电脑安装这个系统,还必须在它们家的软件上编辑,采用类似于数据库导出脚本的形式,每次打印模板都需要打开软件,都不能在自己的项目里直接Api使用,好麻烦真的很累。

卖软件给客户,人家客户就是需要省时省力省事,否则你软件价格就不能卖高,当然操作这么繁琐的,人家也应该不会要。

最后的结果是,伸手党没有,软件不是自己业务需求,怎么办,上级其实都已经准备算了算了,每次有新模板就给他们做一个,这样就会导致很多很多很多数据库模板。

想了想,之前我做过自动化配置表单组件,自由涂鸦画板,那么两者结合起来应该只是时间问题吧,开工~拿人钱财与人方便。

一个草稿版的vue系列POS机小票模板打印系统诞生了,优势:

1.热敏纸尺寸根据你的驱动自己调整大小;

2.本地创建导入数据源的形式,或者本地各种数据源形式作为基础自动字段;

3.有表格,表格列增删改查均可,列还可以开启合计方式,列还可以放置无限行你的数据字段,列还可以放置无限行你自定义的行模板+数据字段,列统一采用底部对其美观,列的宽度可以随意更改,热敏纸宽你就可以放很多列,热敏纸窄你就可以放个位列。

4.有直线、虚线、点线、**线、##线、任意大小标题等等,也有增高垫;

5.有条形码、二维码的图样,可以自定义要关联的基础字段,此字段来源可以本地也可以接口形式;

6.可以上传多张图片,有本地图片库,图片库采用瀑布流的形式,新上传的图片可以任意裁剪成正方形或者长方形(因为有些图片里面有二维码是专门给线下的用户扫码体验的,因为正方形的裁剪是为其服务的,更好的识别);

7.配置的模板多张图片可以随意更换顺序,可重复配置;

8.配置的表格列可以随意更换配置,不影响已开启的合计通道,也可重复配置;

9.配置好的这个模板可以随意调换模板内组件的位置,拖拽实现技术。

10.经过调整,采用lodop虚拟打印机打印出来的效果,与当前配置好的模板视觉效果一致。

由于这个草稿版,因为准备下周开一期迭代进行产品排期与设计实现,到时候列表,模板、后续更多功能,客户体验会大大提升吧。

反正我挖的坑,跪着也得填完~ε=(´ο`*)))唉

草稿版
lodop虚拟打印机打印效果

对哦,我现在放在一个很普通的服务器上,可能后面就被我删了,不过目前短期内还是可以自行体会的。

放出主页源码

<template>
  <div id="spos">
    <!-- 左边菜单栏小控件区域 -->
    <div class="spos_left">
      <!-- 热敏纸尺寸控件 -->
      <div class="spos_panel">
        <div class="spos_metas">
          <span>热敏纸</span>
          <div>
            <span class="spos_tag_size">{{paperSize}}</span>mm
            <!-- 弹出层:热敏纸修改 -->
            <el-popover placement="right">
              <paper-menu :width="paperSize" @change="changePaper" />
              <i slot="reference" class="iconfont icon-bianji" />
            </el-popover>
          </div>
        </div>
      </div>
      <!-- 本地属于源或接口后存放的后台提供字段控件区 -->
      <div class="spos_panel">
        <p class="spos_metas">数据源</p>
        <ol class="spos_tag_wrap">
          <li class="spos_tag" v-for="(tag, tagIndex) in sourceTags" :key="tagIndex" @click="selectTag(tag)">
            {{tag.sqlDesc}}
          </li>
        </ol>
      </div>
      <!-- 小控件区:直线、虚线、点线、**线、##线、增高垫、上传图片、条形码、二维码、标题 -->
      <div class="spos_panel">
        <p class="spos_metas">更多图样</p>
        <ol class="spos_tag_wrap">
          <li class="spos_tag" v-for="(tag, tagIndex) in chartTags" :key="tagIndex" @click="selectTag(tag)">
            {{tag.desc}}
          </li>
        </ol>
      </div>
    </div>
    <!-- 绘制区,可以是空白,也可以是接口获取上级分发给下级的大致模板 -->
    <div class="spos_right"
         v-loading="basicLoading"
         element-loading-text="基础模板获取中"
         element-loading-background="rgba(0, 0, 0, 0.5)">
      <!-- 热敏纸绘制大小与尺寸实时对应 -->
      <div class="spos_canvas" :style="{width: paperSize + 'mm'}">
        <!-- 开启绘制控件拖拽更换位置服务 -->
        <draggable v-model="selectTags"
                   :options="{direction: 'vertical', ghostClass: 'spos_draging'}"
                   @choose="setCurrentTag"
                   @end="focusedTag = -1">
          <!-- 绘制区域已选择的控件集合 -->
          <div class="spos_canvas_meta"
              v-for="(tag, tagIndex) in selectTags" :key="tagIndex"
              :class="{focused: focusedTag === tagIndex}">
            <!-- 控件样式组件 -->
            <source-tag :tag="tag" />
            <!-- 专属于控件的在线可二次利用的编辑 -->
            <div class="spos_operator">
              <el-popover v-if="popoverOF.includes(tag.of)"
                          trigger="click">
                <component :is="`${tag.of}-menu`" :tag="tag" />
                <i slot="reference" class="iconfont icon-xianshimima" />
              </el-popover>
              <i v-if="tag.of === 'field'"
                 class="iconfont icon-shuaxin"
                 @click.stop="reverseTag(tag)" />
              <!-- 删除当前控件 -->
              <i class="iconfont icon-shanchurijie"
                 @click.stop="deleteTag(tagIndex)" />
            </div>
          </div>
        </draggable>
      </div>
      <!-- 测试按钮 -->
      <div class="spos_btns">
        <div class="spos_btn_button" @click="clearSPos">清空模板</div>
        <div class="spos_btn_button" @click="getSPosBaisic">基础模板</div>
        <div class="spos_btn_button" @click="previewSPos">预览</div>
        <div class="spos_btn_button" @click="ajaxPrint">基础模板<br />真实数据<br />假装预览</div>
      </div>
    </div>
    <!-- 本地图片库 -->
    <el-drawer class="spos_drawer"
               :visible.sync="imagesHouse"
               :size="waterfallOptions.containerSize + 'px'"
               :show-close="false"
               append-to-body
               :wrapperClosable="false">
      <div slot="title" class="spos_drawer_title">
        <span>图片库</span>
        <div style="font-size: 0;">
          <el-button type="primary" size="mini" @click="birthImages">生成</el-button>
          <el-button size="mini" @click="closeImageHouse">关闭</el-button>
        </div>
      </div>
      <div class="spos_drawer_content">
        <!-- 上传图片按钮 -->
        <label class="spos_upload_wrapper"
               for="imageToUpload">
          <i class="el-icon-upload" />&nbsp;上传图片
          <div style="font-size: 12px;margin-top: 3px;">支持绝大多数图片格式,图片最大支持2M</div>
          <input id="imageToUpload"
                 class="spos_drawer_file"
                 type="file"
                 accept="image/*"
                 @change="changeImageFile($event)" />
        </label>
        <!-- 图片放置容器 -->
        <div class="spos_images_wrapper">
          <div class="spos_images_list">
            <!-- 采用瀑布流形式更佳观赏 -->
            <ul v-show="sourceImages.length"
                ref="waterfallContainer">
              <li class="spos_images_item"
                  v-for="(item, imIndex) in sourceImages" :key="imIndex"
                  ref="waterfallItem"
                  @click="setImage(item.id)">
                  <img :src="item.src" width="100%" />
                  <p class="spos_images_label">
                    <span>{{item.label}}</span>
                    <i class="iconfont icon-shanchurijie spos_images_delete"
                       @click.stop="deleteImage(imIndex)" />
                  </p>
                  <div v-show="selectImages.includes(item.id)"
                       class="spos_images_right">
                    {{setImage(item.id, 'badge')}}
                  </div>
              </li>
            </ul>
            <div v-if="!sourceImages.length" class="spos_images_empty">暂无本地图片</div>
          </div>
        </div>
      </div>
    </el-drawer>
    <!-- 裁剪图片插件弹窗 -->
    <el-dialog title="上传前剪裁"
               class="spos_cropper_dialog"
               width="500px"
               :visible.sync="cropperDialog"
               :close-on-click-modal="false"
               append-to-body>
      <div class="spos_cropper_content">
        <div class="spos_cropper_descript">
          <span>图片描述:</span>
          <el-input v-model="cropperDescript"
                    clearable
                    size="small"
                    placeholder="更好表达这是张怎么样的图片"
                    style="flex: 1;" />
        </div>
        <!-- 裁剪容器 -->
        <div class="cropper">
          <vueCropper ref="cropper"
                      :img="cropperOptions.img"
                      :info="cropperOptions.info"
                      :outputSize="cropperOptions.outputSize"
                      :autoCrop="cropperOptions.autoCrop"
                      :canMoveBox="cropperOptions.canMoveBox"
                      :original="cropperOptions.original"
                      :centerBox="cropperOptions.centerBox"
                      :infoTrue="cropperOptions.infoTrue" />
        </div>
      </div>
      <div slot="footer" class="dialog-footer">
        <el-button size="small" @click="closeCropperDialog">取消</el-button>
        <el-button size="small" type="primary" @click="finishCropper">确认</el-button>
      </div>
    </el-dialog>
  </div>
</template>

<script>
import PaperMenu from './menu/PaperMenu'
import Draggable from 'vuedraggable'
import sPosPrint from '@/utils/print/sPosPrint' // 调用打印机function
import SourceTag from './handle/SourceTag'
import { SALE, CHARTLET } from './handle/dataSource' // 本地数据源,图样库
import { SALE_IMAGES, SALE_TEMPLATE, SALE_TEST } from './handle/testAjax' // 测试远程数据

const WATERFALL_OPTIONS = { // 瀑布流配置项
  containerSize: 320, // 瀑布流容器
  size: 143, // 每块石头的大小
  space: 10, //每块石头左右间距
  blank: 10 // 每块石头上下间距
}

const CROPPER_OPTIONS = { // 裁剪配置项
  img: '', // 裁剪图片的地址
  info: true, // 裁剪框的大小信息
  outputSize: 1, // 裁剪生成图片的质量
  autoCrop: true, // 是否默认生成截图框
  canMoveBox: true, // 截图框能否拖动
  original: false, // 上传图片按照原始比例渲染
  centerBox: true, // 截图框是否被限制在图片里面
  infoTrue: true // true 为展示真实输出图片宽高 false 展示看到的截图框宽高
}

export default {
  name: 'spos',
  components: {
    PaperMenu, // 热敏纸弹出层可二次编辑内容
    Draggable, // 拖拽插件
    SourceTag, // 绘制区控件样式编辑组件
    TableMenu: () => import('./menu/TableMenu'), // 表格即集合弹窗层可二次编辑内容
    TitleMenu: () => import('./menu/titleMenu'), // 图样标题弹出层可二次编辑内容
    BarcodeMenu: () => import('./menu/barcodeMenu'), // 图样条形码弹出层可二次编辑内容
    QrcodeMenu: () => import('./menu/qrcodeMenu') // 图样二维码弹出层可二次编辑内容
  },
  data() {
    return {
      popoverOF: ['table', 'title', 'barcode', 'qrcode'], // 允许拥有二次编辑权限的控件
      paperSize: 85, // 默认热敏纸初始大小85mm
      imagesHouse: false, // 图片库默认不打开
      waterfallOptions: WATERFALL_OPTIONS, // 瀑布流相关初始化配置
      cropperDialog: false, // 裁剪区域默认不打开
      cropperOptions: CROPPER_OPTIONS, // 裁剪区域相关初始化配置
      cropperDescript: '', // 裁剪区域的关于图片描述的数据绑定
      chartTags: CHARTLET, // 本地图样库
      sourceTags: SALE, // 本地属于源库,也可通过接口获取
      selectTags: [], // 已选择的控件集合
      sourceImages: [], // 服务器上的存储的图片,通过接口获取
      selectImages: [], // 在图片库中已选中的图片们
      codeMaps: [], // 表格可自定义要绑定的字段
      focusedTag: -1, // 绘制区选中的某一个控件,用于编辑或删减目标
      basicLoading: false // 拉取服务器上的上级分发模板的视觉化loading
    }
  },
  created() {
    this.sourceTags.forEach(item => {
      if (item.ofcode === true) { // 查找数据源中拥有表格可自定义标识的字段并初始化它,为了可以重复调用,而不每次循环数据源
        this.codeMaps.push({
          sql: item.sql, sqlDesc: item.sqlDesc, sqlValue: item.ofake
        })
      }
    })
    this.sourceImages = SALE_IMAGES // 假装数据库上的数据已经拉取完毕
  },
  methods: {
    changePaper(nw) { // 更改热敏纸尺寸
      this.paperSize = nw
    },
    reverseTag(tag) { // 调整相邻格式的'单行文本:数据' => '单行文本:   数据'为左右格式
      if (tag.reverse) {
        tag.reverse = !tag.reverse
      } else {
        this.$set(tag, 'reverse', true)
      }
    },
    setCurrentTag(evt) { // 绘制区选中某一控件后,定位它的索引,后续方便操作
      this.focusedTag = evt.oldIndex
    },
    selectTag(tag) { // 选择控件库中的某一控件
      this.focusedTag = -1 // 清除绘制区内处于选中的控件,懒得做重复性的工作了,直接清了更好
      if (tag.of === 'barcode' || tag.of === 'qrcode') { // 如果选择的控件是二维码或者条形码
        this.selectTags.push({ // 需要插入他们可以关联生成的属性,目前codeMaps是可按‘会员编号’‘手工单号’‘核销单号’生成图形
          ...tag, codeMaps: this.codeMaps
        })
      } else if (tag.of === 'images') { // 如果选择的控件是上传图片
        this.imagesHouse = true // 打开图片库
        this._caltureWaterFall() // 重置图片库里的瀑布流格式,以免万一布局错乱不美观
      } else { // 其他控制直接复制添加,复制是为了让vue双向绑定的好处不影响左边菜单栏的数据环境,最好这样做
        this.selectTags.push(JSON.parse(JSON.stringify(tag)))
      }
    },
    deleteTag(tagIndex) { // 删除绘制区的某一控件
      this.selectTags.splice(tagIndex, 1)
      this.focusedTag = -1 // 重置高亮,没有这个也行,有了比较严谨
    },
    setImage(imageId, badge) { // 图片库中选中某些图片,准备生成
      let index = this.selectImages.indexOf(imageId)
      if (badge) { // 记住选中的图片顺序,为了按照鼠标操作顺序上传
        return index + 1
      }
      if (index === -1) {
        this.selectImages.push(imageId)
      } else {
        this.selectImages.splice(index, 1)
      }
    },
    deleteImage(imIndex) { // 删除本地图片库中的图片,后续会调接口,也要去服务器删除
      this.$confirm('此操作将永久删除该文件, 是否继续?', '提示', {
        type: 'warning'
      }).then(_ => {
        this.sourceImages.splice(imIndex, 1)
        this.$nextTick(() => { // 删除成功后要重新计算更新瀑布流位置
          this._renderWaterFall()
        })
      }).catch(_ => {})
    },
    closeCropperDialog() { // 关闭裁剪弹窗
      this.cropperDialog = false
      this.cropperDescript = '' // 清空裁剪的图片备注,更好的客户体验
      document.getElementById('imageToUpload').value = '' // 为了input-file可以不停的上传同一张图片,不受change的影响
    },
    finishCropper() { // 完成裁剪,确认当前裁剪后
      if (!this.cropperDescript) {
        return this.$message.error('请填写相关的图片描述,以便更好的管理图库')
      }
      this.$refs.cropper.getCropBlob(data => { // 裁剪后的图片流
        let cropImage = new Image() // 瀑布流的格式布局必须满足图片类型加载完成后,得到图片的宽高size才能画得更好
        cropImage.src = this._getFileURL(data)
        cropImage.onload = () => {
          this.sourceImages.unshift({ // 瀑布流向前追加新图,倒序排列原则,否则push的话,最新的永远在最底下还得滚动条拉底看到
            id: new Date().getTime(),
            label: this.cropperDescript,
            src: cropImage.src
          })
          this.$nextTick(() => {
            this._renderWaterFall() // 重新对瀑布流布局
            this.closeCropperDialog() // 关闭裁剪框
          })
        }
      })
    },
    changeImageFile(e) { // 上传图片时
      if (!/\.(gif|jpg|jpeg|png|bmp|GIF|JPG|PNG)$/.test(e.target.value)) { 
        return this.$message.error('图片类型必须是.gif,jpeg,jpg,png,bmp中的一种') 
      }
      const files = e.target.files[0]
      if (files.size / 1024 / 1024 > 2) { // lodop虚拟打印机若想打印出图片,那么图片大小不能超越700k,超过都打不出来了
        return this.$message.error('图片不能大于2M')
      }
      this.$nextTick(() => {
        this.cropperOptions.img = this._getFileURL(files) // 赋值给裁剪框,准备裁剪
        this.cropperDialog = true // 打开裁剪框
      })
    },
    closeImageHouse() { // 关闭图片库
      this.imagesHouse = false
      this.selectImages = [] // 关闭后将选中的图片清空,因为可以不停的重复上传,这样比较好看
    },
    birthImages() { // 图片库中“生成”按钮,将选择的图片上传到绘制区
      if (!this.selectImages.length) {
        return this.$message.warning('您还没有选择任何的图片,无法生成')
      }
      let _tag = CHARTLET.find(chart => { // 查找图样中有关上传图片的其他相关属性,绘制区控件组件可能有用
        return chart.of === 'images'
      })
      this.selectTags.push({ // 绘制区控件追加新一个图片控件
        images: this.selectImages.map(id => { // 可能一个控件里生成多张图片,因此数组
          return this.sourceImages.find(data => {
            return data.id === id
          }).src
        }),
        ..._tag
      })
      this.closeImageHouse() // 关闭图片库
    },
    clearSPos() { // 清除当前绘制区,变成白纸
      this.selectTags = []
    },
    getSPosBaisic() { // 获取远程的基础模板,可能是上级分发的
      this.clearSPos() // 先清除可能绘制区内已经绘制过的控件,为了给基础模板腾位置
      this.basicLoading = true // 开启接口加载loading,异步
      setTimeout(() => {
        this.selectTags = SALE_TEMPLATE // 生成新控件数组
        this.basicLoading = false
      }, 500)
    },
    previewSPos() { // 预览当前模板
      if (!this.selectTags.length) {
        return this.$message.warning('当前模板无内容')
      }
      // console.log(JSON.stringify(this.selectTags))
      sPosPrint({ // 调取封装好的打印机方法
        data: null, // 预览过程中不需要传数据,data是为了重打印,或者直接打印存在的,数据与模板相结合时才有值
        selectTags: JSON.parse(JSON.stringify(this.selectTags)), // 控件数组传送
        preview: 1 // 是否预览 1是 0否 默认否
      })
    },
    ajaxPrint() { // 假数据直接打印,因为没有传preview
      sPosPrint({
        data: SALE_TEST, // 模拟假的后台数据
        selectTags: JSON.parse(JSON.stringify(SALE_TEMPLATE))
      })
    },
    _caltureWaterFall() { // 瀑布流的美观性必须满足所有图片都加载完毕,这儿数据量少,不用懒加载了
      if (!this.sourceImages.length) return true
      let load_image_length = 0
      this.sourceImages.forEach(item => { // 每张图片循环
        let waterfallImage = new Image()
        waterfallImage.src = item.src
        waterfallImage.onload = () => { //当所有图片加载成功后,才开始构建瀑布流
          load_image_length++
          if (load_image_length === SALE_IMAGES.length) { // 所有图片都加载完毕后
            this._renderWaterFall() // 计算瀑布流高度与渲染图片绝对位置
          }
        }
      })
    },
    _renderWaterFall() { // 绘制瀑布流
      const distance = this.waterfallOptions.size + this.waterfallOptions.space // 每颗石头尺寸至少是它本身宽度+相邻间距大小
      const count = Math.floor(this.waterfallOptions.containerSize / distance) // 一行能容纳多少颗石头,并向下取整
      if (count <= 0) return true // 一块都容不下去了
      let array = this.$refs.waterfallItem // 所有的石子
      let arrayHeights = [] // 每一行的高度集合
      for (let i = 0; i < array.length; i++) {
        let j = i % count
        array[i].style.width = this.waterfallOptions.size + 'px'
        if (arrayHeights.length === count) { // 一行排满后,自动切换至下一行
          let minIndex = this._findWaterFallMinIndex(arrayHeights)
          array[i].style.left = (distance * minIndex) + 'px'
          array[i].style.top = (arrayHeights[minIndex] + this.waterfallOptions.blank) + 'px'
          arrayHeights[minIndex] += (array[i].offsetHeight + this.waterfallOptions.blank)
        } else{
          arrayHeights[j] = array[i].offsetHeight
          array[i].style.left = (distance * j) + 'px'
          array[i].style.top =  0
        }
      }
      this._resetWaterFallHeight(count, arrayHeights) // 重置这个瀑布容器的高度
    },
    _findWaterFallMinIndex(heights) { // 寻找在当前所有瀑布中,高度最小的那条索引
      let m = 0
      for (let z = 0; z < heights.length; z++) {
        m = Math.min(heights[m], heights[z]) === heights[m] ? m : z // 取最矮的那个高度索引
      }
      return m
    },
    _resetWaterFallHeight(count, arrayHeights) { // 瀑布流中每颗石头都是绝对定位,要重置容器高度,才有滚动条可以操作
      let a = 0
      if (arrayHeights.length > 1) {
        for (let b = 0; b < Math.min(count, arrayHeights.length); b++) {
          a = Math.max(arrayHeights[a], arrayHeights[b]) === arrayHeights[a] ? a : b // 取最高的那个高度索引
        }
      }
      this.$refs.waterfallContainer.style.height = arrayHeights[a] + 'px'
    },
    _getFileURL(file) { // 将input-file转化成本地流文件路径,获取上传图片的尺寸大小或其他
      let url = null
      if (window.createObjectURL != undefined) { // basic
        url = window.createObjectURL(file)
      }else if (window.webkitURL != undefined) { // webkit or chrome
        url = window.webkitURL.createObjectURL(file)
      }else if (window.URL != undefined) { // mozilla(firefox)
        url = window.URL.createObjectURL(file)
      }
      return url
    }
  }
}
</script>

<style scoped>
#spos {
  height: 100%;
  display: flex;
  flex-direction: row;
  background-color: #eee;
  overflow: hidden;
}
.spos_left {
  width: 235px;
  overflow: auto;
  background-color: #fff;
}
.spos_panel {
  padding: 8px 12px;
}
.spos_metas {
  color: #666;
  font-size: 15px;
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
}
.spos_tag_wrap { 
  font-size: 0;
}
.spos_tag {
  width: 48%;
  display: inline-block;
  vertical-align: top;
  list-style-type: none;
  padding: 6px 10px;
  background-color: #d8d8d8;
  border: 1px solid #bda5a5;
  margin-top: 10px;
  font-size: 12px;
  box-sizing: border-box;
}
.spos_tag:nth-child(2n) {
  margin-left: 4%;
}
.spos_tag_size {
  font-size: 20px;
  color: orange;
}
.spos_metas .icon-bianji {
  color: orange;
  font-size: 20px;
  vertical-align: bottom;
  cursor: pointer;
}
.spos_right {
  flex: 1;
  position: relative;
}
.spos_btns {
  position: absolute;
  right: 0;
  top: 0;
}
.spos_btn_button {
  margin-top: 12px;
  background-color: orange;
  color: #fff;
  min-width: 60px;
  border-top-left-radius: 4px;
  border-bottom-left-radius: 4px;
  padding: 10px;
  text-align: center;
  font-size: 14px;
  cursor: pointer;
}
.spos_canvas {
  background-color: #fff;
  margin: 0 auto;
  overflow: auto;
  box-sizing: border-box;
  padding: 10px 6px;
  height: 100%;
}
.spos_canvas_meta {
  position: relative;
  box-sizing: border-box;
}
.spos_canvas_meta .spos_operator {
  display: none;
  color: #fff;
  position: absolute;
  right: 0;
  bottom: 0;
  cursor: pointer;
  background-color: rgba(56, 42, 16, 0.45);
  padding: 1px 6px;
}
.spos_canvas_meta.focused {
  border: 2px dotted orange;
}
.spos_canvas_meta.focused .spos_operator {
  display: block;
}
.spos_draging {
  border: 2px dotted orange;
  cursor: move;
}
.spos_draging .spos_operator {
  background-color: #fff!important;
  display: none!important;
}
.spos_drawer /deep/ .el-drawer__header{
  margin-bottom: 0;
  padding: 0;
}
.spos_drawer_title {
  padding: 12px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.spos_drawer_content {
  height: 100%;
  display: flex;
  flex-direction: column;
}
.spos_upload_wrapper {
  background-color: lightcyan;
  padding: 10px;
  text-align: center;
  color: #666;
  position: relative;
  cursor: pointer;
}
.spos_upload_wrapper .el-icon-upload {
  font-size: 30px;
  margin-top: -5px;
  vertical-align: middle;
}
.spos_drawer_file {
  position: absolute;
  top: 0;
  left: 0;
  display: none;
}
.spos_images_wrapper {
  flex: 1;
  position: relative;
}
.spos_images_empty {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate3d(-50%, -50%, 0);
  color: #999;
  font-size: 14px;
}
.spos_images_list {
  position: absolute;
  top: 10px;
  left: 12px;
  right: 12px;
  bottom: 10px;
  overflow: auto;
}
.spos_images_item {
  position: absolute;
  font-size: 12px;
  border-radius: 4px;
  overflow: hidden;
  box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
  cursor: pointer;
}
.spos_images_right {
  position: absolute;
  right: 5px;
  top: 5px;
  background-color: #409EFF;
  color: #fff;
  font-size: 12px;
  height: 18px;
  line-height: 18px;
  padding: 0 5px;
  text-align: center;
  white-space: nowrap;
  border-radius: 10px;
}
.spos_images_label {
  padding: 6px 8px;
  display: flex;
  justify-content: space-between;
}
.spos_images_delete {
  color: orange;
  font-size: 14px;
}
.spos_cropper_dialog /deep/ .el-dialog__body {
  padding: 0 12px;
}
.spos_cropper_descript {
  display: flex;
  align-items: center;
  margin-bottom: 15px;
  margin-top: 5px;
}
.spos_cropper_content .cropper {
  width: auto;
  height: 300px;
  text-align: center;
}
</style>

main.js配置

import Vue from 'vue'
import App from './App'
// 注册elementUI
import Element from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(Element)
// 注册阿里巴巴iconfont
import './styles/icon/iconfont.css'
import './styles/normalize.css'
// 注册裁剪插件
import VueCropper from 'vue-cropper'
Vue.use(VueCropper)

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  render: h => h(App)
})

代办银行转账凭条代做ATM转账回执单小票南宁定制转账回执单苏州银行转账小票价格宁德银行回执单作用泉州汇款凭证吉林定做银行转账凭条武汉汇款凭证代开临沂代开跨行转账凭条珠海办理手机银行电子回单贵阳办定期存单福州办ATM汇款转账小票北京转账回执单制作常州转账凭条哪家专业莆田制作手机银行电子回单蚌埠银行转账小票制作遵义转账凭条多少钱宁德制作转账小票银川手机银行电子回单定制泰州定做银行转账小票贵阳转账回执单哪家好蚌埠转账凭条哪家好许昌柜台转账汇款凭证定制合肥代做银行柜台转账凭证盐城定期存单查询新乡ATM转账回执单小票用途成都转账回执单服务商无锡开银行回执单淮安汇款凭条开具潍坊银行柜台转账凭证哪里有揭阳汇款回执单哪家好香港通过《维护国家安全条例》两大学生合买彩票中奖一人不认账让美丽中国“从细节出发”19岁小伙救下5人后溺亡 多方发声卫健委通报少年有偿捐血浆16次猝死汪小菲曝离婚始末何赛飞追着代拍打雅江山火三名扑火人员牺牲系谣言男子被猫抓伤后确诊“猫抓病”周杰伦一审败诉网易中国拥有亿元资产的家庭达13.3万户315晚会后胖东来又人满为患了高校汽车撞人致3死16伤 司机系学生张家界的山上“长”满了韩国人?张立群任西安交通大学校长手机成瘾是影响睡眠质量重要因素网友洛杉矶偶遇贾玲“重生之我在北大当嫡校长”单亲妈妈陷入热恋 14岁儿子报警倪萍分享减重40斤方法杨倩无缘巴黎奥运考生莫言也上北大硕士复试名单了许家印被限制高消费奥巴马现身唐宁街 黑色着装引猜测专访95后高颜值猪保姆男孩8年未见母亲被告知被遗忘七年后宇文玥被薅头发捞上岸郑州一火锅店爆改成麻辣烫店西双版纳热带植物园回应蜉蝣大爆发沉迷短剧的人就像掉进了杀猪盘当地回应沈阳致3死车祸车主疑毒驾开除党籍5年后 原水城县长再被查凯特王妃现身!外出购物视频曝光初中生遭15人围殴自卫刺伤3人判无罪事业单位女子向同事水杯投不明物质男子被流浪猫绊倒 投喂者赔24万外国人感慨凌晨的中国很安全路边卖淀粉肠阿姨主动出示声明书胖东来员工每周单休无小长假王树国卸任西安交大校长 师生送别小米汽车超级工厂正式揭幕黑马情侣提车了妈妈回应孩子在校撞护栏坠楼校方回应护栏损坏小学生课间坠楼房客欠租失踪 房东直发愁专家建议不必谈骨泥色变老人退休金被冒领16年 金额超20万西藏招商引资投资者子女可当地高考特朗普无法缴纳4.54亿美元罚金浙江一高校内汽车冲撞行人 多人受伤

代办银行转账凭条 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化