| <template> | 
|   <div :style="canvasStyle" class="canvas-container" @mousedown="selectWidget=null;"> | 
|     <div class="canvas-content"> | 
|       <draggable v-selectable="data.fields" v-model="data.fields" :options="{group:'people', ghostClass: 'ghost', filter:'.detail-table'}" class="drag-list" @end="handleMoveEnd" @add="handleWidgetAdd"> | 
|         <template v-for="(element, index) in data.fields"> | 
|           <DraggableResizable v-bind.sync="element.options" :key="index" :class="{'active': isActive(element)}" :style="'z-index:' + element.options.z" :data-key="element.key" class="resable-item" @click.native.stop="handleSelectWidget(index)"> | 
|             <template v-if="element.type=='table'"> | 
|               <table :style="{'border-color':element.borderColor, 'width':element.width}" class="detail-table" @mousedown.stop> | 
|                 <thead v-if="element.showHeader"> | 
|                   <draggable v-model="element.fields" :options="{group:'detail', ghostClass: 'ghost', animation:300, chosenClass:'chosen-item'}" :class="{'drag-th':true}" tag="tr"> | 
|                     <th v-for="(th, thIndex) in element.fields" :key="thIndex" :style="{'border-color':element.borderColor, 'width':th.width+'px'}" :class="{'drag-active': isActive(th)}" @click.stop="handleThSelectWidget(th)"> | 
|                       <div :style="getHeaderDivStyle(th)" class="cell">{{ th.label }}</div> | 
|                       <div class="tool"> | 
|                         <el-button title="删除" class="widget-action-delete" circle plain type="danger" @click.stop="handleDetailWidgetDelete(element.fields, thIndex)"> | 
|                           <i class="el-icon-yrt-shanchu2"></i> | 
|                         </el-button> | 
|                       </div> | 
|                     </th> | 
|                   </draggable> | 
|                 </thead> | 
|                 <tbody> | 
|                   <tr v-for="i in [0]" :key="'tr'+i"> | 
|                     <td v-for="(th, thIndex) in element.fields" :key="thIndex" :style="{'border-color':element.borderColor}"> | 
|                       <div class="cell"></div> | 
|                     </td> | 
|                   </tr> | 
|                 </tbody> | 
|               </table> | 
|               <div class="tool"> | 
|                 <el-button title="删除" class="widget-action-delete" circle plain type="danger" @click.stop="handleWidgetDelete(index)"> | 
|                   <i class="el-icon-yrt-shanchu2"></i> | 
|                 </el-button> | 
|                 <el-button title="添加字段" class="widget-action-addplus" circle plain type="primary" @click.stop="handleDetailWidgetAdd(element, index)"> | 
|                   <i class="el-icon-yrt-addplus"></i> | 
|                 </el-button> | 
|               </div> | 
|             </template> | 
|             <template v-else-if="element.type=='field'"> | 
|               <div :style="element.styles" :class="{'drag-active': isActive(element)}" class="text-box"> | 
|                 <div :style="element.label.styles" class="label">{{ element.label.title }} | 
|                   <template v-if="element.label.showColon">:</template> | 
|                 </div> | 
|                 <div :style="element.text.styles" class="text">{ {{ element.text.prop }} }</div> | 
|               </div> | 
|               <div class="tool"> | 
|                 <el-button v-if="element.options.w>50" title="删除" class="widget-action-delete" circle plain type="danger" @click.stop="handleWidgetDelete(index)"> | 
|                   <i class="el-icon-yrt-shanchu2"></i> | 
|                 </el-button> | 
|                 <!-- <el-button title="复制" class="widget-action-clone" circle plain type="primary" @click.stop="handleWidgetClone(index)"> | 
|                   <i class="el-icon-yrt-fuzhi4"></i> | 
|                 </el-button> --> | 
|               </div> | 
|             </template> | 
|             <template v-else-if="element.type=='barcode'"> | 
|               <div :style="element.styles" :class="{'drag-active': isActive(element)}" class="text-box"> | 
|                 <vue-barcode :value="element.barcode.value" :options="element.barcode.options"></vue-barcode> | 
|               </div> | 
|               <div class="tool"> | 
|                 <el-button v-if="element.options.w>50" title="删除" class="widget-action-delete" circle plain type="danger" @click.stop="handleWidgetDelete(index)"> | 
|                   <i class="el-icon-yrt-shanchu2"></i> | 
|                 </el-button> | 
|                 <!-- <el-button title="复制" class="widget-action-clone" circle plain type="primary" @click.stop="handleWidgetClone(index)"> | 
|                   <i class="el-icon-yrt-fuzhi4"></i> | 
|                 </el-button> --> | 
|               </div> | 
|             </template> | 
|             <template v-else-if="element.type=='qrcode'"> | 
|               <div :style="element.styles" :class="{'drag-active': isActive(element)}" class="text-box"> | 
|                 <vue-qrcode :value="element.qrOptions.value" :options="element.qrOptions"></vue-qrcode> | 
|               </div> | 
|               <div class="tool"> | 
|                 <el-button v-if="element.options.w>50" title="删除" class="widget-action-delete" circle plain type="danger" @click.stop="handleWidgetDelete(index)"> | 
|                   <i class="el-icon-yrt-shanchu2"></i> | 
|                 </el-button> | 
|               </div> | 
|             </template> | 
|             <!-- 图片上传 --> | 
|             <template v-else-if="element.type=='image'"> | 
|               <div :style="element.styles" :class="{'drag-active': isActive(element)}" class="text-box"> | 
|                 <el-upload :action="common.domain + element.image.serverAction" :show-file-list="false" :http-request="(params) => {uploadHttp(params, element)}" :before-upload="handleBeforeUpload" :headers="headers" :style="{width:element.image.width+'mm', height:element.image.height+'mm'}" class="img-uploader"> | 
|                   <img v-if="element.image.imageUrl" :src="element.image.imageUrl" :style="{width:element.image.width+'mm', height:element.image.height+'mm'}" class="img"> | 
|                   <i v-else class="el-icon-plus img-uploader-icon"></i> | 
|                 </el-upload> | 
|               </div> | 
|               <div class="tool"> | 
|                 <el-button v-if="element.options.w>50" title="删除" class="widget-action-delete" circle plain type="danger" @click.stop="handleWidgetDelete(index)"> | 
|                   <i class="el-icon-yrt-shanchu2"></i> | 
|                 </el-button> | 
|                 <!-- <el-button title="复制" class="widget-action-clone" circle plain type="primary" @click.stop="handleWidgetClone(index)"> | 
|                   <i class="el-icon-yrt-fuzhi4"></i> | 
|                 </el-button> --> | 
|               </div> | 
|             </template> | 
|             <template v-else-if="['line-horizontal', 'line-vertical'].indexOf(element.type)>=0"> | 
|               <div :style="element.styles" :class="{'drag-active': isActive(element)}" class="text-box"> | 
|               </div> | 
|               <div class="tool"> | 
|                 <el-button v-if="element.options.w>50" title="删除" class="widget-action-delete" circle plain type="danger" @click.stop="handleWidgetDelete(index)"> | 
|                   <i class="el-icon-yrt-shanchu2"></i> | 
|                 </el-button> | 
|                 <!-- <el-button title="复制" class="widget-action-clone" circle plain type="primary" @click.stop="handleWidgetClone(index)"> | 
|                   <i class="el-icon-yrt-fuzhi4"></i> | 
|                 </el-button> --> | 
|               </div> | 
|             </template> | 
|             <template v-else-if="['pagination'].indexOf(element.type)>=0"> | 
|               <div :style="element.styles" :class="{'drag-active': isActive(element)}" class="pagination-box"> | 
|                 <div class="label">{{ element.layout }}</div> | 
|               </div> | 
|               <div class="tool"> | 
|                 <el-button v-if="element.options.w>50" title="删除" class="widget-action-delete" circle plain type="danger" @click.stop="handleWidgetDelete(index)"> | 
|                   <i class="el-icon-yrt-shanchu2"></i> | 
|                 </el-button> | 
|                 <!-- <el-button title="复制" class="widget-action-clone" circle plain type="primary" @click.stop="handleWidgetClone(index)"> | 
|                   <i class="el-icon-yrt-fuzhi4"></i> | 
|                 </el-button> --> | 
|               </div> | 
|             </template> | 
|             <template v-else> | 
|               <div :style="element.styles" :class="{'drag-active': isActive(element)}" class="text-box"> | 
|                 <div :style="element.label.styles" class="label">{{ element.label.title }}</div> | 
|               </div> | 
|               <div class="tool"> | 
|                 <el-button v-if="element.options.w>50" title="删除" class="widget-action-delete" circle plain type="danger" @click.stop="handleWidgetDelete(index)"> | 
|                   <i class="el-icon-yrt-shanchu2"></i> | 
|                 </el-button> | 
|                 <!-- <el-button title="复制" class="widget-action-clone" circle plain type="primary" @click.stop="handleWidgetClone(index)"> | 
|                   <i class="el-icon-yrt-fuzhi4"></i> | 
|                 </el-button> --> | 
|               </div> | 
|             </template> | 
|           </DraggableResizable> | 
|         </template> | 
|       </draggable> | 
|     </div> | 
|   </div> | 
| </template> | 
|   | 
| <script> | 
| import { selectable } from "./directive/selectable.js"; | 
| import Draggable from "vuedraggable"; | 
| import DraggableResizable from "@/components/base/draggable-resizable"; | 
| import VueBarcode from "@xkeshi/vue-barcode"; | 
| import VueQrcode from "@chenfengyuan/vue-qrcode"; | 
| import ossClient from "@/utils/aliyun.oss.client"; | 
|   | 
| export default { | 
|   components: { | 
|     Draggable, | 
|     DraggableResizable, | 
|     VueBarcode, | 
|     VueQrcode | 
|   }, | 
|   directives: { | 
|     selectable | 
|   }, | 
|   props: { | 
|     data: { | 
|       type: Object, | 
|       default: null | 
|     }, | 
|     select: { | 
|       type: Object, | 
|       default: null | 
|     }, | 
|     configType: { | 
|       type: String, | 
|       default: null | 
|     }, | 
|     detailFields: { | 
|       type: Array, | 
|       default: null | 
|     } | 
|   }, | 
|   data() { | 
|     return { | 
|       selectWidget: this.select | 
|     }; | 
|   }, | 
|   computed: { | 
|     currentConfigType: { | 
|       get: function() { | 
|         return this.configType; | 
|       }, | 
|       set: function(val) { | 
|         this.$emit("update:config-type", val); | 
|       } | 
|     }, | 
|     // 当前选中明细列表 | 
|     currentDetailFields: { | 
|       get: function() { | 
|         return this.detailFields; | 
|       }, | 
|       set: function(val) { | 
|         this.$emit("update:detail-fields", val); | 
|       } | 
|     }, | 
|     // 获得当前是否激活 | 
|     isActive: function() { | 
|       return function(th) { | 
|         var active = this.selectWidget && this.selectWidget.key === th.key; | 
|         return active; | 
|       }; | 
|     }, | 
|     // 画布样式设置 | 
|     canvasStyle: function() { | 
|       var opts = this.data.dataOptions; | 
|       return { | 
|         width: opts.width + "mm", | 
|         height: opts.height + "mm", | 
|         "padding-top": opts.paddingTop + "mm", | 
|         "padding-bottom": opts.paddingBottom + "mm", | 
|         "padding-left": opts.paddingLeft + "mm", | 
|         "padding-right": opts.paddingRight + "mm" | 
|       }; | 
|     }, | 
|     // 上传header参数 | 
|     headers: function() { | 
|       var timestamp = new Date().valueOf(); | 
|       var nonce = Math.random() | 
|         .toString(36) | 
|         .substr(2); | 
|       var guid = this.common.getUserGuid(); | 
|       var userInfo = this.common.getUserInfo(); | 
|       var accessToken = userInfo.accessToken; | 
|       var tokenInfo = this.common.getTokenInfo(); | 
|       var params = { | 
|         noDataSign: true | 
|       }; | 
|   | 
|       var signature = this.common.getSignature(timestamp, nonce, tokenInfo, params); | 
|       var opts = { | 
|         timestamp: timestamp, | 
|         nonce: nonce, | 
|         guid: guid, | 
|         accessToken: accessToken, | 
|         signature: signature, | 
|         subFolder: "print", // 设置子文件夹 | 
|         noDataSign: true // 数据不参与加密 | 
|       }; | 
|       return opts; | 
|     } | 
|   }, | 
|   watch: { | 
|     select(val) { | 
|       this.selectWidget = val; | 
|     }, | 
|     selectWidget: { | 
|       handler(val) { | 
|         this.$emit("update:select", val); | 
|       }, | 
|       deep: true | 
|     }, | 
|     data: { | 
|       handler(val) {}, | 
|       deep: true | 
|     } | 
|   }, | 
|   methods: { | 
|     handleMoveEnd({ newIndex, oldIndex }) {}, | 
|     handleMoveStart({ oldIndex }) {}, | 
|     // 元素选中 | 
|     handleSelectWidget(index) { | 
|       this.selectWidget = this.data.fields[index]; | 
|       this.currentConfigType = "WidgetConfig"; | 
|     }, | 
|     // 明细单元格选中 | 
|     handleThSelectWidget(th, index) { | 
|       this.selectWidget = th; | 
|       this.currentConfigType = "WidgetConfig"; | 
|     }, | 
|     handleWidgetAdd(evt) { | 
|       const newIndex = evt.newIndex; | 
|   | 
|       // 为拖拽到容器的元素添加唯一 key | 
|       const key = Date.parse(new Date()) + "_" + Math.ceil(Math.random() * 99999); | 
|       var newField = JSON.parse(JSON.stringify(this.data.fields[newIndex])); | 
|       this.$set(this.data.fields, newIndex, { ...newField, key }); | 
|       delete this.data.fields[newIndex].icon; | 
|   | 
|       if (this.data.fields[newIndex].type === "table") { | 
|         var _fields = this.data.fields[newIndex].fields; | 
|         _fields.forEach((_field, _index) => { | 
|           const _key = Date.parse(new Date()) + "_" + Math.ceil(Math.random() * 99999) + _index; | 
|           this.$set(_field, "key", _key); | 
|         }); | 
|       } | 
|   | 
|       this.currentConfigType = "WidgetConfig"; | 
|       this.selectWidget = this.data.fields[newIndex]; | 
|     }, | 
|     // 删除 | 
|     handleWidgetDelete(index) { | 
|       if (this.data.fields.length - 1 === index) { | 
|         if (index === 0) { | 
|           this.selectWidget = {}; | 
|         } else { | 
|           this.selectWidget = this.data.fields[index - 1]; | 
|         } | 
|       } else { | 
|         this.selectWidget = this.data.fields[index + 1]; | 
|       } | 
|   | 
|       this.$nextTick(() => { | 
|         this.data.fields.splice(index, 1); | 
|       }); | 
|     }, | 
|     // 复制 | 
|     handleWidgetClone(index) { | 
|       const cloneData = { | 
|         ...JSON.parse(JSON.stringify(this.data.fields[index])), | 
|         key: Date.parse(new Date()) + "_" + Math.ceil(Math.random() * 99999) | 
|       }; | 
|       cloneData.options.x += 5; | 
|       cloneData.options.y += 5; | 
|   | 
|       this.data.fields.splice(index, 0, cloneData); | 
|   | 
|       this.$nextTick(() => { | 
|         this.selectWidget = this.data.fields[index + 1]; | 
|       }); | 
|     }, | 
|     // 删除明细字段 | 
|     handleDetailWidgetDelete(fields, index) { | 
|       if (fields.length - 1 === index) { | 
|         if (index === 0) { | 
|           this.selectWidget = {}; | 
|         } else { | 
|           this.selectWidget = fields[index - 1]; | 
|         } | 
|       } else { | 
|         this.selectWidget = fields[index + 1]; | 
|       } | 
|   | 
|       this.$nextTick(() => { | 
|         fields.splice(index, 1); | 
|       }); | 
|     }, | 
|     // 新增明细字段 | 
|     handleDetailWidgetAdd(element, index) { | 
|       this.$set(element.fields, element.fields.length, { | 
|         type: "detail-field", | 
|         prop: "", | 
|         width: 150, | 
|         header: { | 
|           fontSize: 14, | 
|           textAlign: "center", | 
|           lineHeight: 20 | 
|         }, | 
|         body: { | 
|           fontSize: 14, | 
|           textAlign: "left", | 
|           lineHeight: 20 | 
|         }, | 
|         label: "字段" + element.fields.length, | 
|         key: Date.parse(new Date()) + "_" + Math.ceil(Math.random() * 99999) | 
|       }); | 
|     }, | 
|     // 表格按下事件 | 
|     onThMouseDown(field, $event) {}, | 
|     // 上传至阿里云 | 
|     uploadHttp(params, element) { | 
|       const fileName = `upload/print/${this.common.formatDate(new Date(), "YYYY-MM-DD")}/${new Date().valueOf()}-${this.common.getGUID()}.jpg`; // 定义唯一的文件名 | 
|       ossClient(this.common.uploadConf) | 
|         .put(fileName, params.file, { | 
|           ContentType: "image/jpeg" | 
|         }) | 
|         .then(({ res, url, name }) => { | 
|           if (res && res.status === 200) { | 
|             element.image.imageUrl = res.requestUrls[0]; | 
|           } | 
|         }) | 
|         .catch(err => { | 
|           this.$message.error(`上传图片失败` + err.message); | 
|         }); | 
|     }, | 
|     // 上传图片前 | 
|     handleBeforeUpload(file) { | 
|       const isImg = ["image/jpeg", "image/png"].indexOf(file.type) >= 0; | 
|       const isLt2M = file.size / 1024 / 1024 < 2; | 
|   | 
|       if (!isImg) { | 
|         this.$message.error("上传头像图片只能是 JPG/PNG 格式!"); | 
|       } | 
|       if (!isLt2M) { | 
|         this.$message.error("上传头像图片大小不能超过 2MB!"); | 
|       } | 
|       return isImg && isLt2M; | 
|     }, | 
|     // 表格头部div样式 | 
|     getHeaderDivStyle(th) { | 
|       const data = { "font-size": th.header.fontSize + "mm", "line-height": th.header.lineHeight + "mm", "text-align": th.header.textAlign }; | 
|       if (th.width) { | 
|         data.width = th.width + "px"; | 
|       } | 
|       return data; | 
|     } | 
|   } | 
| }; | 
| </script> | 
|   | 
| <style lang="scss" scoped> | 
| .canvas-container { | 
|   -moz-box-shadow: 0px 0px 10px #3d3d3d; | 
|   -webkit-box-shadow: 0px 0px 10px #3d3d3d; | 
|   box-shadow: 0px 0px 10px #3d3d3d; | 
|   margin: 20px 6px; | 
|   .canvas-content { | 
|     height: 100%; | 
|     padding-bottom: 0px; | 
|     border: 1px dotted #000000; | 
|     .drag-list { | 
|       height: 100%; | 
|       position: relative; | 
|     } | 
|   | 
|     .resable-item { | 
|       &.active { | 
|         border: 1px dashed #888; | 
|       } | 
|       &.selected { | 
|         border: 1px dashed #888; | 
|       } | 
|     } | 
|     .tool { | 
|       display: none; | 
|       position: absolute; | 
|       right: 0; | 
|       bottom: -30px; | 
|       z-index: 100; | 
|     } | 
|     .active { | 
|       background-color: rgb(213, 234, 255); | 
|       > .tool { | 
|         display: block; | 
|       } | 
|     } | 
|     .text-box { | 
|       overflow: hidden; | 
|       white-space: nowrap; | 
|       .label { | 
|         width: 120px; | 
|         display: inline-block; | 
|         text-align: right; | 
|       } | 
|       .text { | 
|         display: inline-block; | 
|       } | 
|     } | 
|   } | 
|   | 
|   .ghost { | 
|     background: #fff; | 
|     border: 1px dashed #409eff; | 
|   | 
|     &::after { | 
|       background: #fff; | 
|     } | 
|   } | 
|   | 
|   li.ghost { | 
|     height: 30px; | 
|     list-style: none; | 
|     font-size: 0; | 
|   } | 
|   | 
|   .detail-table { | 
|     border-collapse: collapse; | 
|     border: 1px solid #dcdfe6; | 
|     background-color: white; | 
|     th, | 
|     td { | 
|       font-weight: normal; | 
|       position: relative; | 
|       border-collapse: collapse; | 
|       border: 1px solid #dcdfe6; | 
|       padding: 3px 2px; | 
|       .cell { | 
|         min-height: 20px; | 
|       } | 
|       &.drag-active { | 
|         background-color: rgb(213, 234, 255); | 
|         .tool { | 
|           display: block; | 
|         } | 
|       } | 
|     } | 
|   } | 
|   | 
|   // 选择框 | 
|   /deep/ .selectable { | 
|     position: absolute; | 
|     width: 0px; | 
|     height: 0px; | 
|     background-color: #409eff20; | 
|     border: 1px solid #409eff70; | 
|   } | 
|   | 
|   // 图片上传 | 
|   /deep/ .img-uploader { | 
|     .el-upload { | 
|       border: 1px dashed #d9d9d9; | 
|       border-radius: 6px; | 
|       cursor: pointer; | 
|       position: relative; | 
|       overflow: hidden; | 
|       &:hover { | 
|         border-color: #409eff; | 
|       } | 
|     } | 
|     .img-uploader-icon { | 
|       font-size: 28px; | 
|       color: #8c939d; | 
|       width: 100px; | 
|       height: 60px; | 
|       line-height: 60px; | 
|       text-align: center; | 
|     } | 
|     .img { | 
|       width: 100px; | 
|       height: 60px; | 
|       display: block; | 
|     } | 
|   } | 
| } | 
| </style> |