Browse Source

标签打印

master
han\hanst 8 months ago
parent
commit
d050ea43d2
  1. 6
      src/api/labelSetting/label_setting.js
  2. 14
      src/utils/zplUtil.js
  3. 59
      src/utils/zplUtil2.js
  4. 2
      src/views/modules/labelSetting/com_add_update_customer_label.vue
  5. 2
      src/views/modules/labelSetting/com_add_update_default_label.vue
  6. 26
      src/views/modules/labelSetting/com_add_update_label.vue
  7. 244
      src/views/modules/labelSetting/label_draw.vue
  8. 566
      src/views/modules/labelSetting/label_draw2.vue
  9. 25
      src/views/modules/labelSetting/label_setting.vue

6
src/api/labelSetting/label_setting.js

@ -5,3 +5,9 @@ export const getLabelSettingList = data => createAPI('/label/setting/getLabelSet
// 删除标签的信息
export const deleteLabelSetting = data => createAPI('/label/setting/deleteLabelSetting','post',data)
// 添加标签的信息
export const saveZplElements = data => createAPI('/label/setting/saveZplElements','post',data)
// 修改标签的信息
export const getZplElements = data => createAPI('/label/setting/getZplElements','post',data)

14
src/utils/zplUtil.js

@ -1,5 +1,6 @@
export function generateZPL(elements) {
export function generateZPL(elements,zplHOrP) {
const zpl = ['^XA'];
zpl.push(zplHOrP)
elements.forEach(el => {
const x = Math.round(el.x);
const y = Math.round(el.y * 1.5); // 画布到 ZPL 像素转换
@ -10,6 +11,17 @@ export function generateZPL(elements) {
zpl.push(el.newline
? `^FO${x},${y}^FB${el.lineWidth},${el.lineRows},0^CFJ,${el.fontSize}^FD${el.data}^FS`
: `^FO${x},${y}^FD${el.data}^FS`);
if (el.bold) {
zpl.push(el.newline
? `^FO${x+1},${y}^FB${el.lineWidth},${el.lineRows},0^CFJ,${el.fontSize}^FD${el.data}^FS`
: `^FO${x+1},${y}^FD${el.data}^FS`);
zpl.push(el.newline
? `^FO${x},${y+1}^FB${el.lineWidth},${el.lineRows},0^CFJ,${el.fontSize}^FD${el.data}^FS`
: `^FO${x},${y+1}^FD${el.data}^FS`);
zpl.push(el.newline
? `^FO${x+1},${y+1}^FB${el.lineWidth},${el.lineRows},0^CFJ,${el.fontSize}^FD${el.data}^FS`
: `^FO${x+1},${y+1}^FD${el.data}^FS`);
}
break;
case 'barcode':

59
src/utils/zplUtil2.js

@ -0,0 +1,59 @@
export function generateZPL(elements,zplHOrP) {
const zpl = ['^XA'];
//zpl.push('^POI');
zpl.push(zplHOrP)
elements.forEach(el => {
const x = Math.round(el.y * 1.3);
const y = Math.round((750-el.x) * 1.5); // 你之前的转换比例保持不变
switch (el.type) {
case 'text':
zpl.push(`^CI28^LH0,^JUS^CWJ,E:SIMSUN.FNT^CFJ,${el.fontSize},${el.fontSize}`);
zpl.push(el.newline
? `^FO${x},${y}^FB${el.lineWidth},${el.lineRows},0^CFJ,${el.fontSize}^FD${el.data}^FS`
: `^FO${x},${y}^FD${el.data}^FS`);
if (el.bold) {
zpl.push(el.newline
? `^FO${x+1},${y}^FB${el.lineWidth},${el.lineRows},0^CFJ,${el.fontSize}^FD${el.data}^FS`
: `^FO${x+1},${y}^FD${el.data}^FS`);
zpl.push(el.newline
? `^FO${x},${y+1}^FB${el.lineWidth},${el.lineRows},0^CFJ,${el.fontSize}^FD${el.data}^FS`
: `^FO${x},${y+1}^FD${el.data}^FS`);
zpl.push(el.newline
? `^FO${x+1},${y+1}^FB${el.lineWidth},${el.lineRows},0^CFJ,${el.fontSize}^FD${el.data}^FS`
: `^FO${x+1},${y+1}^FD${el.data}^FS`);
}
break;
case 'barcode':
zpl.push(`^FO${x},${y}^BY${el.width}^BCB,${el.height},Y,N,N^FD${el.data}^FS`);
break;
case 'qrcode':
zpl.push(`^FO${x},${y}^BQB,2,${el.height},^FDLA,${el.data}^FS`);
break;
case 'onecode':
zpl.push(`^FO${x},${y}^BY${el.width}^BCB,${el.height},^FD${el.data}^FS`);
break;
case 'pic':
if (el.data) {
zpl.push(`^FO${x},${y}^GFA,${el.data}`);
}
break;
case 'hLine':
const y1 = Math.round(1200-el.x)-el.width;
zpl.push(`^FO${x},${y1}^FWR^GB${el.height},${el.width},3,B^FS`);
break;
case 'vLine':
zpl.push(`^FO${x},${y}^FWR^GB${el.height},${el.width},3,B^FS`);
break;
}
});
zpl.push('^XZ');
return zpl.join('\n');
}

2
src/views/modules/labelSetting/com_add_update_customer_label.vue

@ -180,7 +180,7 @@ export default {
//
this.pageData.username = this.userId;
//
this.getMultiLanguageList();
//this.getMultiLanguageList();
//
this.titleCon = this.labels.titleCon;
},

2
src/views/modules/labelSetting/com_add_update_default_label.vue

@ -156,7 +156,7 @@ export default {
//
this.pageData.checkFlag = 'Y';
//
this.getMultiLanguageList();
//this.getMultiLanguageList();
//
this.titleCon = this.labels.titleCon;
},

26
src/views/modules/labelSetting/com_add_update_label.vue

@ -7,36 +7,40 @@
<el-row>
<el-col :span="12">
<el-form-item class="customer-item" :label=labels.labelNo>
<el-input v-model="pageData.labelNo" :readonly="labelNoReadOnly" style="width: 120px;" ></el-input>
<el-input v-model="pageData.labelNo" :readonly="labelNoReadOnly" ></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item class="customer-item" :label=labels.labelType>
<el-select v-model="pageData.labelType" style="width: 150px;" >
<el-select v-model="pageData.labelType" >
<el-option label="外箱标签" value="外箱标签"></el-option>
<el-option label="卷标签" value="卷标签"></el-option>
</el-select >
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item class="customer-item" :label=labels.printDirection>
<el-select v-model="pageData.printDirection" >
<el-option label="横向打印" value="横向打印"></el-option>
<el-option label="纵向打印" value="纵向打印"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item class="customer-item" :label=labels.labelName>
<el-input v-model="pageData.labelName" style="width: 120px;" ></el-input>
<el-input v-model="pageData.labelName" ></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item class="customer-item" :label=labels.labelClass>
<el-select v-model="pageData.labelClass" style="width: 150px;" >
<el-select v-model="pageData.labelClass" >
<el-option label="打印软件" value="打印软件"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-col :span="12">
<el-form-item class="customer-item" :label=labels.remark>
<el-input v-model="pageData.remark" style="width: 310px;" ></el-input>
<el-input v-model="pageData.remark" ></el-input>
</el-form-item>
</el-col>
</el-row>
@ -85,6 +89,7 @@ export default {
labelClass: '',
remark: '',
addFlag: false,
printDirection : '横向打印',
},
labelNoReadOnly: false,
dataListLoading: false,
@ -104,6 +109,7 @@ export default {
labelType: '标签类型:',
labelName: '报表文件名:',
labelClass: '标签种类:',
printDirection: '打印方向:',
remark: '备注:',
confirmLabel: '确认',
cancelLabel: '取消',

244
src/views/modules/labelSetting/label_draw.vue

@ -2,7 +2,13 @@
<div id="app" class="container">
<!-- 左侧组件库 -->
<div class="component-palette">
<h3>工具栏</h3>
<span class="reportId"><el-input style="font-size: 18px;margin-bottom: 10px" placeholder="标签编号" v-model="labelNo" ></el-input></span>
<el-checkbox v-model="showGrid" style="margin-bottom: 10px; margin-left: 5px">显示网格</el-checkbox>
<el-checkbox v-model="snapToGrid" style="margin-bottom: 10px; margin-left: 5px">吸附到网格</el-checkbox>
<label style="margin-left: 5px">网格大小</label>
<el-input v-model="gridSize" :min="5" :max="100" :step="5" size="mini" style="margin-left: 5px"></el-input>
<h1 style="margin-left: 5px">工具栏</h1>
<div class="draggable-item" data-type="text" draggable @dragstart="onDragStart">
<i class="el-icon-document"></i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;文本</div>
<div class="draggable-item" data-type="barcode" draggable @dragstart="onDragStart">
@ -21,7 +27,17 @@
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;竖线</div>
</div>
<!-- 设计画布 -->
<div class="design-area" ref="container" style="height: 500;width: 500" @dragover.prevent @drop="onDrop">
<div
class="design-area"
ref="container"
:style="[showGrid ? gridStyle : {}, {height: '860px', width: '720px'}]"
@dragover.prevent
@drop="onDrop"
>
<!-- 提示阴影文字 -->
<div v-if="elements.length === 0" class="canvas-hint-text">
拖入组件开始设计
</div>
<div
draggable
v-for="(el, index) in elements"
@ -84,33 +100,42 @@
<!-- StringBuilder StringBuffer util height width transition动画效果 延迟显示等 -->
<!-- 右侧属性面板 -->
<div class="property-panel">
<div v-if="elements.length === 0" class="property-hint-text">
属性以及预览区域
</div>
<div style="height: 140px">
<div v-if="selectedElement">
<el-tooltip placement="top">
<div slot="content">宽度高度等属性需遵循ZPL指令协议比如二维码最大高度是10大于10不生效</div>
<h3 style="width: 100px">属性配置 <i class="el-icon-info"></i></h3>
<h3 style="width: 100px">属性配置 <i @click="previewZPL" class="el-icon-info"></i></h3>
</el-tooltip>
<div v-if="selectedElement.type === 'text'">
<label>文本内容<input v-model="selectedElement.data"/></label><br><br>
<label>字体大小<input type="number" v-model="selectedElement.fontSize"/></label>
<br><br><label>自动换行<input v-model="selectedElement.newline" type="checkbox"/></label>
<br><br> <label v-if="selectedElement.newline">
文本宽度<input type="number" v-model="selectedElement.lineWidth"/></label><br><br>
<label>文本内容<el-input style="width: 240px" v-model="selectedElement.data"/></label>
<el-button style="margin-left: 5px" type="primary" @click="dataFromShow">数据源</el-button>
<br><br>
<label>字体大小<el-input style="width: 240px" type="number" v-model="selectedElement.fontSize"/></label>
<br><br>
<label style="font: bold">加粗<el-checkbox v-model="selectedElement.bold"></el-checkbox></label>
<label style="margin-left: 10px">自动换行<el-checkbox v-model="selectedElement.newline"></el-checkbox></label>
<label v-if="selectedElement.newline">
文本行数<input type="number" v-model="selectedElement.lineRows"/></label>
文本宽度<el-input type="number" style="width: 80px" v-model="selectedElement.lineWidth"/></label>
<label v-if="selectedElement.newline">
文本行数<el-input type="number" style="width: 80px" v-model="selectedElement.lineRows"/></label>
</div>
<div v-if="selectedElement.type === 'hLine' || selectedElement.type === 'vLine'">
<label>宽度<input type="number" v-model="selectedElement.width"/></label><br><br>
<label>高度<input type="number" v-model="selectedElement.height"/></label>
<label>宽度<el-input type="number" style="width: 240px" v-model="selectedElement.width"/></label><br><br>
<label>高度<el-input type="number" style="width: 240px" v-model="selectedElement.height"/></label>
</div>
<div v-if="selectedElement.type === 'barcode' || selectedElement.type === 'onecode'">
<label>数据<input v-model="selectedElement.data"/></label><br><br>
<label>宽度<input type="number" v-model="selectedElement.width"/></label><br><br>
<label>高度<input type="number" v-model="selectedElement.height"/></label>
<label>数据<el-input style="width: 240px" v-model="selectedElement.data"/></label>
<el-button style="margin-left: 5px" type="primary" @click="dataFromShow">数据源</el-button><br><br>
<label>宽度<el-input type="number" style="width: 240px" v-model="selectedElement.width"/></label><br><br>
<label>高度<el-input type="number" style="width: 240px" v-model="selectedElement.height"/></label>
</div>
<div v-if="selectedElement.type === 'qrcode'">
<label>数据<input v-model="selectedElement.data"/></label><br><br>
<label>高度<input type="number" @change="checkValue" v-model="selectedElement.height"/></label>
<label>数据<el-input style="width: 240px" v-model="selectedElement.data"/></label>
<el-button style="margin-left: 5px" type="primary" @click="dataFromShow">数据源</el-button><br><br>
<label>高度<el-input type="number" style="width: 240px" @change="checkValue" v-model="selectedElement.height"/></label>
</div>
<div v-if="selectedElement.type === 'pic'">
<i icon="el-icon-upload"></i><input type="file" ref="uploadFile" accept="image/*" @change="handleImageUpload"/>
@ -118,9 +143,15 @@
</div>
</div>
</div>
<div style="height: 750px">
<div style="height: 750px" v-if="elements.length > 0">
<el-button type="danger" @click="deleteElement" icon="el-icon-delete">删除</el-button>
<el-button type="primary" @click="saveZPL" icon="el-icon-document">保存</el-button>
<el-button type="primary" @click="saveZPL" icon="el-icon-document">保存</el-button><br><br>
<el-select v-model="zplDpi">
<el-option value="6" label="152dpi"/>
<el-option value="8" label="203dpi"/>
<el-option value="12" label="300dpi"/>
<el-option value="24" label="600dpi"/>
</el-select>
<el-button type="primary" @click="previewZPL" icon="el-icon-document">预览</el-button><br><br>
<img v-if="previewImage" :src="previewImage" style="width: 60%;height: auto;border: 1px solid rgba(200, 200, 200, 0.8); " alt="ZPL预览图">
<el-tooltip placement="top">
@ -130,15 +161,35 @@
<pre id="zplcode" style="font-size: 14px">{{ generatedZPL }}</pre>
</div>
</div>
<el-dialog title="数据源" :close-on-click-modal="false" v-drag :visible.sync="dataFromFlag" width="500px">
<el-form label-position="top" >
<el-checkbox-group v-model="checkedKeys">
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px;margin-left: 10px;margin-top: 10px">
<el-checkbox v-for="key in dataKeyList" :key="key" :label="key"
style="margin: 0; align-items: center; display: flex;">{{ key }}</el-checkbox>
</div>
</el-checkbox-group>
</el-form>
<el-footer style="height:40px;margin-top: 80px;text-align:center">
<el-button type="primary" @click="dataConfirm">确定</el-button>
<el-button type="primary" @click="dataFromFlag=false">关闭</el-button>
</el-footer>
</el-dialog>
</div>
</template>
<script>
import axios from 'axios';
import debounce from 'lodash/debounce';
import { generateZPL } from '@/utils/zplUtil.js';
import {saveZplElements,getZplElements} from '@/api/labelSetting/label_setting.js';
export default {
data() {
return {
zplDpi: '8',
zplHOrP:'^FWN',
canvas: null,
context: null,
previewDataUrl: '',
@ -151,6 +202,14 @@ export default {
selectedIndex: -1,
dpi: 203, //
dragType: '',//12
labelNo:'',
showGrid: true, //
snapToGrid: true,
gridSize: 20,
labelSet: {},
dataFromFlag:false,
dataKeyList:[],
checkedKeys :[],
}
},
computed: {
@ -158,10 +217,42 @@ export default {
return this.elements[this.selectedIndex]
},
generatedZPL() {
return generateZPL(this.elements);
return generateZPL(this.elements,this.zplHOrP);
},
gridStyle() {
const size = this.gridSize || 10;
return {
backgroundImage: 'linear-gradient(to right, #eee 1px, transparent 1px), linear-gradient(to bottom, #eee 1px, transparent 1px)',
backgroundSize: `${size}px ${size}px`,
backgroundColor: '#fff'
};
}
},
created() {
// this
this.debouncedPreviewZPL = debounce(this.previewZPL, 500); // 500ms
},
activated() {
this.labelNo = this.$route.params.labelSetting?this.$route.params.labelSetting.labelNo:this.labelNo;
this.labelSet = this.$route.params.labelSetting?this.$route.params.labelSetting:this.labelSet;
this.getZplElements();
},
watch: {
generatedZPL: function (newVal, oldVal) {
if (newVal) {
this.debouncedPreviewZPL();
}
}
},
methods: {
dataFromShow () {
this.dataKeyList=Object.keys(this.elements[0])
this.dataFromFlag = true;
},
dataConfirm () {
this.elements[this.selectedIndex].data = this.checkedKeys.map(key => `#{${key}}`).join('');;
this.dataFromFlag = false;
},
handleImageUpload(event) {
const file = event.target.files[0];
if (!file) return;
@ -180,13 +271,11 @@ export default {
this.canvas = canvas;
this.context = ctx;
this.previewDataUrl = canvas.toDataURL();
this.generatePicZPL();
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
setTimeout(()=>{
this.generatePicZPL();
}, 100)
},
generatePicZPL() {
@ -232,9 +321,16 @@ export default {
//
handleDrag(e) {
if (this.dragIndex === -1) return;
//
const newX = e.clientX - this.startPos.x;
const newY = e.clientY - this.startPos.y;
let newX = e.clientX - this.startPos.x;
let newY = e.clientY - this.startPos.y;
// newX / newY
if (this.snapToGrid && this.gridSize > 0) {
newX = Math.round(newX / this.gridSize) * this.gridSize;
newY = Math.round(newY / this.gridSize) * this.gridSize;
}
this.$set(this.elements[this.dragIndex], 'x', newX);
this.$set(this.elements[this.dragIndex], 'y', newY);
},
@ -264,6 +360,12 @@ export default {
e.dataTransfer.setData('type', e.target.dataset.type)
},
onDrop(e) {
if (!this.labelNo) {
this.$alert('标签编号不可为空!', '错误', {
confirmButtonText: '确定'
})
return false
}
const type = e.dataTransfer.getData('type');
const rect = e.target.getBoundingClientRect();
const x = e.clientX - rect.left;
@ -272,14 +374,15 @@ export default {
if (this.dragType === 1) {
const newElement = {
type, x, y,
bold: false,//
newline:false,//
lineRows: 2,//
lineWidth: 200,//
fontSize: 30,
data: "",
//
height: type == 'hLine' ? 3 : type == 'qrcode' || type === 'pic' ? 10 : type == 'vLine' ? 200 : 50,
width: type == 'hLine' ? 200 : type == 'onecode' ? 10 : 3
height: type == 'hLine' ? 3 : type == 'qrcode' || type === 'pic' ? 10 : type == 'vLine' ? 600 : 50,
width: type == 'hLine' ? 600 : type == 'onecode' ? 3 : 3,
}
this.elements.push(newElement)
if (type=='pic') {
@ -304,21 +407,39 @@ export default {
},
saveZPL() {
let dataIn = {}
dataIn.zplcode = this.generatedZPL;
dataIn.zplCode = this.generatedZPL;
dataIn.elements = this.elements;
ajax.$post('/labelHist/saveZPL', dataIn, resp => {
if (resp.status === 200) {
this.$message({message: '保存成功',
type: 'success', duration: 1500, onClose: () => {}
})
this.elements = resp.data.rows;
dataIn.reportId = this.labelNo;
if (!this.labelNo) {
this.$alert('标签编号不可为空!', '错误', {
confirmButtonText: '确定'
})
return false
}
saveZplElements(dataIn).then(({data}) => {
if(data.code == 0){
this.$message.success(data.msg);
}else{
this.$message.error(data.msg);
}
});
})
},
getZplElements () {
getZplElements({reportId: this.labelNo}).then(({data}) => {
if(data.code == 200){
this.elements = data.data;
if (this.elements.length > 0) {
this.previewZPL();
}
}else{
this.$message.error(data.msg);
}
})
},
async previewZPL() {
try {
const response = await axios.post(
'https://api.labelary.com/v1/printers/8dpmm/labels/4x6/0/',
'https://api.labelary.com/v1/printers/'+ this.zplDpi +'dpmm/labels/4x6/0/',
this.generatedZPL,
{ responseType: 'blob' }
);
@ -380,17 +501,17 @@ export default {
padding: 2px; /* 内边距 */
transition: transform 0.3s ease, box-shadow 0.2s ease; /* 动画效果 */
user-select: none; /* 禁止选中文本 */
background-color: #fbfbfb; /* 背景颜色 */
background-color: #f1f1f1; /* 背景颜色 */
}
.design-element.active {
border: 2px solid #00d7ff; /* 选中元素边框 */
box-shadow: 0 8px 12px rgba(0, 123, 255, 0.3); /* 选中元素阴影 */
border: 1px solid #00d7ff; /* 选中元素边框 */
box-shadow: 0 4px 6px rgba(0, 123, 255, 0.3); /* 选中元素阴影 */
}
.design-element:hover {
transform: scale(1.05); /* 鼠标悬停时放大 */
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15); /* 鼠标悬停阴影 */
transform: scale(1.02); /* 鼠标悬停时放大 */
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.15); /* 鼠标悬停阴影 */
}
.design-element svg {
@ -405,11 +526,11 @@ export default {
font-weight: 600;
color: #222;
padding: 10px 16px;
border: 1px solid #ddd;
// border: 1px solid #ddd;
border-radius: 10px;
background-color: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease;
background-color: #f1f1f1;
//box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1);
//transition: transform 0.2s ease;
}
.property-panel {
@ -425,4 +546,37 @@ export default {
.preview {
margin: 0 10px;
}
.canvas-hint-text {
position: absolute;
top: 40%;
left: 48%;
transform: translate(-50%, -50%);
font-size: 32px;
font-weight: 600;
color: rgba(150, 150, 150, 0.25); /* 柔和灰色 */
font-style: italic;
pointer-events: none;
user-select: none;
z-index: 1;
letter-spacing: 2px;
}
.property-hint-text {
position: absolute;
top: 40%;
left: 78%;
transform: translate(-50%, -50%);
font-size: 32px;
font-weight: 600;
color: rgba(150, 150, 150, 0.25); /* 柔和灰色 */
font-style: italic;
pointer-events: none;
user-select: none;
z-index: 1;
letter-spacing: 2px;
}
/deep/.reportId input.el-input__inner {
height: 28px !important;
}
</style>

566
src/views/modules/labelSetting/label_draw2.vue

@ -0,0 +1,566 @@
<template>
<div id="app" class="container">
<!-- 左侧组件库 -->
<div class="component-palette">
<span class="reportId"><el-input style="font-size: 18px;margin-bottom: 5px" placeholder="标签编号" v-model="labelNo" ></el-input></span>
<el-checkbox v-model="showGrid" style="margin-bottom: 10px; margin-left: 5px">显示网格</el-checkbox>
<el-checkbox v-model="snapToGrid" style="margin-bottom: 10px; margin-left: 5px">吸附到网格</el-checkbox>
<label style="margin-left: 5px">网格大小</label>
<el-input v-model="gridSize" :min="5" :max="100" :step="5" size="mini" style="margin-left: 5px"></el-input>
<h1 style="margin-left: 5px">工具栏</h1>
<div class="draggable-item" data-type="text" draggable @dragstart="onDragStart">
<i class="el-icon-document"></i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;文本</div>
<div class="draggable-item" data-type="barcode" draggable @dragstart="onDragStart">
<i style="display: inline-block; transform: rotate(90deg);" class="el-icon-tickets"></i>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;条码</div>
<div class="draggable-item" data-type="qrcode" draggable @dragstart="onDragStart">
<i class="el-icon-menu"></i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;二维码</div>
<div class="draggable-item" data-type="onecode" draggable @dragstart="onDragStart">
<i class="el-icon-more"></i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;一维码</div>
<div class="draggable-item" data-type="pic" draggable @dragstart="onDragStart">
<i class="el-icon-picture"></i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;图片</div>
<div class="draggable-item" data-type="hLine" draggable @dragstart="onDragStart">
<i class="el-icon-minus"></i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;横线</div>
<div class="draggable-item" data-type="vLine" draggable @dragstart="onDragStart">
<i style="display: inline-block; transform: rotate(90deg);" class="el-icon-minus"></i>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;竖线</div>
</div>
<!-- 设计画布 -->
<div
class="design-area"
ref="container"
:style="[showGrid ? gridStyle : {}, {height: '670px', width: '890px'}]"
@dragover.prevent
@drop="onDrop"
>
<!-- 提示阴影文字 -->
<div v-if="elements.length === 0" class="canvas-hint-text">
拖入组件开始设计
</div>
<div
draggable
v-for="(el, index) in elements"
:key="index"
class="design-element"
:style="{ left: el.x + 'px', top: el.y + 'px' }"
@mousedown="startDrag(index, $event)"
@click="selectElement(index)"
:class="{ active: selectedIndex === index }">
<!-- 文本图标 -->
<div v-if="el.type === 'text'" class="text-preview">
文本元素
</div>
<!-- 条形码图标 -->
<div v-if="el.type === 'barcode'" class="barcode-preview">
<svg width="128" height="64" viewBox="0 0 24 24" fill="none">
<path d="M2 4v16M4 4v16M6 4v16M8 4v16M10 4v16M12 4v16M14 4v16M16 4v16M18 4v16M20 4v16M22 4v16" stroke="#333" stroke-width="1"/>
</svg>
<div class="label" style="text-align: center">条码</div>
</div>
<!-- 一维码图标 -->
<div v-if="el.type === 'onecode'" class="onecode-preview">
<svg width="128" height="64" viewBox="0 0 24 24" fill="none">
<rect x="2" y="4" width="2" height="16" fill="#333"/>
<rect x="6" y="4" width="1" height="16" fill="#333"/>
<rect x="9" y="4" width="2" height="16" fill="#333"/>
<rect x="13" y="4" width="1" height="16" fill="#333"/>
<rect x="16" y="4" width="2" height="16" fill="#333"/>
<rect x="20" y="4" width="1" height="16" fill="#333"/>
</svg>
<div class="label" style="text-align: center">一维码</div>
</div>
<!-- 二维码图标 -->
<svg v-if="el.type === 'qrcode'" width="100" height="100" viewBox="0 0 24 24" fill="none">
<rect x="2" y="2" width="6" height="6" stroke="#333" stroke-width="2"/>
<rect x="16" y="2" width="6" height="6" stroke="#333" stroke-width="2"/>
<rect x="2" y="16" width="6" height="6" stroke="#333" stroke-width="2"/>
<rect x="10" y="10" width="4" height="4" fill="#333"/>
<rect x="14" y="14" width="2" height="2" fill="#333"/>
</svg>
<!-- 图片图标 -->
<svg v-if="el.type === 'pic'" width="100" height="100" viewBox="0 0 24 24" fill="none">
<rect x="2" y="4" width="20" height="16" stroke="#333" stroke-width="2"/>
<circle cx="8" cy="10" r="2" fill="#333"/>
<path d="M2 18l6-6 4 4 6-6 4 4v4H2z" fill="#aaa"/>
</svg>
<!-- 水平线 -->
<div v-if="el.type === 'hLine'"
:style="{ width: (el.width>500?(el.width-260):(el.width-100)) + 'px', height: el.height + 'px', backgroundColor: '#000', borderRadius: '1px' }"></div>
<!-- 垂直线 -->
<div v-if="el.type === 'vLine'"
:style="{ width: el.width + 'px', height: (el.height-150) + 'px', backgroundColor: '#000', borderRadius: '1px' }"></div>
</div>
</div>
<!-- StringBuilder StringBuffer util height width transition动画效果 延迟显示等 -->
<!-- 右侧属性面板 -->
<div class="property-panel">
<div v-if="elements.length === 0" class="property-hint-text">
属性预览区域
</div>
<div style="height: 140px">
<div v-if="selectedElement">
<el-tooltip placement="top">
<div slot="content">宽度高度等属性需遵循ZPL指令协议比如二维码最大高度是10大于10不生效</div>
<h3 style="width: 100px">属性配置 <i class="el-icon-info"></i></h3>
</el-tooltip>
<div v-if="selectedElement.type === 'text'">
<label>文本内容<input v-model="selectedElement.data"/></label><br><br>
<label>字体大小<input type="number" v-model="selectedElement.fontSize"/></label>
<br><br>
<label style="font: bold">加粗<el-checkbox v-model="selectedElement.bold"></el-checkbox></label>
<label style="margin-left: 10px">自动换行<el-checkbox v-model="selectedElement.newline"></el-checkbox></label>
<label v-if="selectedElement.newline">
文本宽度<input type="number" v-model="selectedElement.lineWidth"/></label>
<label v-if="selectedElement.newline">
文本行数<input type="number" v-model="selectedElement.lineRows"/></label>
</div>
<div v-if="selectedElement.type === 'hLine' || selectedElement.type === 'vLine'">
<label>宽度<input type="number" v-model="selectedElement.width"/></label><br><br>
<label>高度<input type="number" v-model="selectedElement.height"/></label>
</div>
<div v-if="selectedElement.type === 'barcode' || selectedElement.type === 'onecode'">
<label>数据<input v-model="selectedElement.data"/></label><br><br>
<label>宽度<input type="number" v-model="selectedElement.width"/></label><br><br>
<label>高度<input type="number" v-model="selectedElement.height"/></label>
</div>
<div v-if="selectedElement.type === 'qrcode'">
<label>数据<input v-model="selectedElement.data"/></label><br><br>
<label>高度<input type="number" @change="checkValue" v-model="selectedElement.height"/></label>
</div>
<div v-if="selectedElement.type === 'pic'">
<i icon="el-icon-upload"></i><input type="file" ref="uploadFile" accept="image/*" @change="handleImageUpload"/>
<img :src="previewDataUrl" class="preview" :style="{ width: '120px', border: '1px solid #ccc' }" />
</div>
</div>
</div>
<div style="height: 750px" v-if="elements.length > 0">
<el-button type="danger" @click="deleteElement" icon="el-icon-delete">删除</el-button>
<el-button type="primary" @click="saveZPL" icon="el-icon-document">保存</el-button><br><br>
<el-select v-model="zplDpi">
<el-option value="6" label="152dpi"/>
<el-option value="8" label="203dpi"/>
<el-option value="12" label="300dpi"/>
<el-option value="24" label="600dpi"/>
</el-select>
<el-button type="primary" @click="previewZPL" icon="el-icon-document">预览</el-button><br><br>
<img v-if="previewImage" :src="previewImage"
style="width: 65%;height: auto;margin-left: 60px;
border: 1px solid rgba(200, 200, 200, 0.8); transform: rotate(90deg); " alt="ZPL预览图">
<el-tooltip placement="top">
<div slot="content">画布布局仅供参考精确布局请查看下面的预览图</div>
<h3 style="width: 100px">ZPL代码 <i class="el-icon-info"></i></h3>
</el-tooltip>
<pre id="zplcode" style="font-size: 14px">{{ generatedZPL }}</pre>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios';
import { generateZPL } from '@/utils/zplUtil2.js';
import debounce from "lodash/debounce";
import {saveZplElements,getZplElements} from '@/api/labelSetting/label_setting.js';
export default {
data() {
return {
zplDpi: '8',
zplHOrP:'^FWB',
canvas: null,
context: null,
previewDataUrl: '',
zplCode: '',
imgWidth: 200, // 8
previewImage:'',
dragIndex: -1,
startPos: {x: 0, y: 0},
elements: [], //
selectedIndex: -1,
dpi: 203, //
dragType: '',//12
labelNo:'',
showGrid: true, //
snapToGrid: true,
gridSize: 20,
labelSet: {},
}
},
computed: {
selectedElement() {
return this.elements[this.selectedIndex]
},
generatedZPL() {
return generateZPL(this.elements,this.zplHOrP);
},
gridStyle() {
const size = this.gridSize || 10;
return {
backgroundImage: 'linear-gradient(to right, #eee 1px, transparent 1px), linear-gradient(to bottom, #eee 1px, transparent 1px)',
backgroundSize: `${size}px ${size}px`,
backgroundColor: '#fff'
};
}
},
created() {
// this
this.debouncedPreviewZPL = debounce(this.previewZPL, 500); // 500ms
},
activated() {
this.labelNo = this.$route.params.labelSetting?this.$route.params.labelSetting.labelNo:this.labelNo;
this.labelSet = this.$route.params.labelSetting?this.$route.params.labelSetting:this.labelSet;
this.getZplElements();
},
watch: {
generatedZPL: function (newVal, oldVal) {
if (newVal) {
this.debouncedPreviewZPL();
}
}
},
methods: {
handleImageUpload(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
// dataURL
this.previewDataUrl = e.target.result;
const img = new Image();
img.onload = () => {
const scale = this.imgWidth / img.width;
// = × scale = × scale
const rotatedCanvas = document.createElement('canvas');
rotatedCanvas.width = Math.round(img.height * scale);
rotatedCanvas.height = this.imgWidth;
const ctx = rotatedCanvas.getContext('2d');
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, rotatedCanvas.width, rotatedCanvas.height);
// 90 = -90°
ctx.translate(0, rotatedCanvas.height);
ctx.rotate(-Math.PI / 2);
//
ctx.drawImage(img, 0, 0, img.width, img.height,
0, 0, this.imgWidth, Math.round(img.height * scale));
this.canvas = rotatedCanvas;
this.context = ctx;
//this.previewDataUrl = rotatedCanvas.toDataURL('image/jpeg', 0.6);
this.generatePicZPL();
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
},
generatePicZPL() {
if (!this.canvas || !this.context) return;
const imgData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
const { width, height, data } = imgData;
const bytesPerRow = Math.ceil(width / 8);
const totalBytes = bytesPerRow * height;
let binaryData = '';
for (let y = 0; y < height; y++) {
for (let byteIdx = 0; byteIdx < bytesPerRow; byteIdx++) {
let byteStr = '';
for (let bit = 0; bit < 8; bit++) {
const x = byteIdx * 8 + bit;
if (x >= width) {
byteStr += '0';
} else {
const i = (y * width + x) * 4;
const r = data[i], g = data[i + 1], b = data[i + 2];
const grayscale = (r + g + b) / 3;
byteStr += grayscale < 128 ? '1' : '0';
}
}
const hex = parseInt(byteStr, 2).toString(16).padStart(2, '0').toUpperCase();
binaryData += hex;
}
}
this.selectedElement.data = `${totalBytes},${totalBytes},${bytesPerRow},${binaryData}`;
},
//
startDrag(index, e) {
this.dragType = 2;
this.dragIndex = index;
this.startPos = {
x: e.clientX - this.elements[index].x,
y: e.clientY - this.elements[index].y
};
e.preventDefault();//
e.stopPropagation();//,mouseup
document.addEventListener('mousemove', this.handleDrag);
document.addEventListener('mouseup', this.stopDrag);
},
//
handleDrag(e) {
if (this.dragIndex === -1) return;
let newX = e.clientX - this.startPos.x;
let newY = e.clientY - this.startPos.y;
// newX / newY
if (this.snapToGrid && this.gridSize > 0) {
newX = Math.round(newX / this.gridSize) * this.gridSize;
newY = Math.round(newY / this.gridSize) * this.gridSize;
}
this.$set(this.elements[this.dragIndex], 'x', newX);
this.$set(this.elements[this.dragIndex], 'y', newY);
},
//
stopDrag(e) {
e.preventDefault();
const containerRect = this.$refs.container.getBoundingClientRect();
const mouseX = e.clientX;
const mouseY = e.clientY;
//
if ( mouseX < containerRect.left || mouseX > containerRect.right || mouseY < containerRect.top || mouseY > containerRect.bottom) {
this.$message({message: '不可超出画布区域',
type: 'error', duration: 2000, onClose: () => {}
})
return;
}
this.dragIndex = -1;
document.removeEventListener('mousemove', this.handleDrag);
document.removeEventListener('mouseup', this.stopDrag);
//
this.previewZPL();
},
onDragStart(e) {
this.dragType = 1;
e.dataTransfer.setData('type', e.target.dataset.type)
},
onDrop(e) {
if (!this.labelNo) {
this.$alert('标签编号不可为空!', '错误', {
confirmButtonText: '确定'
})
return false
}
const type = e.dataTransfer.getData('type');
const rect = e.target.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// 1
if (this.dragType === 1) {
const newElement = {
type, x, y,
newline:false,//
lineRows: 2,//
lineWidth: 200,//
fontSize: 30,
data: "",
//
height: type == 'hLine' ? 3 : type == 'qrcode' || type === 'pic' ? 10 : type == 'vLine' ? 600 : 50,
width: type == 'hLine' ? 600 : type == 'onecode' ? 5 : 3
}
this.elements.push(newElement)
if (type=='pic') {
this.canvas = '';
this.context = '';
this.previewDataUrl = '';
if (this.$refs.uploadFile) {
this.$refs.uploadFile.value = null;
}
}
}
},
deleteElement() {
this.$confirm(`确定要删除这个元素吗`, '提示', {
confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
}).then(() => {
this.elements.splice(this.selectedIndex, 1);
})
},
selectElement(index) {
this.selectedIndex = index
},
saveZPL() {
let dataIn = {}
dataIn.zplCode = this.generatedZPL;
dataIn.elements = this.elements;
dataIn.reportId = this.labelNo;
if (!this.labelNo) {
this.$alert('标签编号不可为空!', '错误', {
confirmButtonText: '确定'
})
return false
}
saveZplElements(dataIn).then(({data}) => {
if(data.code === 0){
this.$message.success("保存成功!");
}else{
this.$message.error(data.msg);
}
})
},
getZplElements () {
getZplElements({reportId: this.labelNo}).then(({data}) => {
if(data.code === 200){
this.elements = data.data;
if (this.elements.length > 0) {
this.previewZPL();
}
}else{
this.$message.error(data.msg);
}
})
},
async previewZPL() {
try {
const response = await axios.post(
'https://api.labelary.com/v1/printers/'+ this.zplDpi +'dpmm/labels/4x6/0/',
this.generatedZPL,
{ responseType: 'blob' }
);
this.previewImage = URL.createObjectURL(response.data);
} catch (error) {
console.error('预览失败:', error);
}
},
checkValue() {
if (this.selectedElement.height > 10) {
this.$message({message: '二维码最大尺寸为10',
type: 'error', duration: 1500, onClose: () => {}
})
this.selectedElement.height = 10;
}
},
}
}
</script>
<style scoped>
.container {
display: flex;
height: 100%;
overflow: auto;
}
.component-palette {
width: 120px;
//border-right: 1px solid #ddd;
padding: 10px;
}
.draggable-item {
padding: 8px;
margin: 5px;
border: 1px solid #999;
cursor: move;
margin-top: 20px;
}
.design-area {
flex: 1;
position: relative;
border: 1px dashed #ccc;
width: 890px;
height: 670px;
}
.design-element {
position: absolute;
}
.design-element {
position: absolute;
cursor: move;
border-radius: 8px; /* 圆角 */
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); /* 阴影效果 */
padding: 2px; /* 内边距 */
transition: transform 0.3s ease, box-shadow 0.2s ease; /* 动画效果 */
user-select: none; /* 禁止选中文本 */
background-color: #f1f1f1; /* 背景颜色 */
}
.design-element.active {
border: 2px solid #00d7ff; /* 选中元素边框 */
box-shadow: 0 8px 12px rgba(0, 123, 255, 0.3); /* 选中元素阴影 */
}
.design-element:hover {
transform: scale(1.05); /* 鼠标悬停时放大 */
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15); /* 鼠标悬停阴影 */
}
.design-element svg {
border-radius: 4px; /* 圆角 */
}
.design-element div {
border-radius: 4px; /* 圆角 */
}
.text-preview {
font-size: 20px;
font-weight: 600;
color: #222;
padding: 10px 16px;
//border: 1px solid #ddd;
border-radius: 10px;
background-color: #f1f1f1;
//box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
//transition: transform 0.2s ease;
}
.property-panel {
width: 400px;
//border-left: 1px solid #ddd;
padding: 10px;
}
.image-zpl-uploader {
max-width: 600px;
margin: 20px auto;
}
.preview {
margin: 0 10px;
}
.canvas-hint-text {
position: absolute;
top: 40%;
left: 48%;
transform: translate(-50%, -50%);
font-size: 32px;
font-weight: 600;
color: rgba(150, 150, 150, 0.25); /* 柔和灰色 */
font-style: italic;
pointer-events: none;
user-select: none;
z-index: 1;
letter-spacing: 2px;
}
.property-hint-text {
position: absolute;
top: 40%;
left: 82%;
transform: translate(-50%, -50%);
font-size: 32px;
font-weight: 700;
color: rgba(150, 150, 150, 0.25); /* 柔和灰色 */
font-style: italic;
pointer-events: none;
user-select: none;
z-index: 1;
letter-spacing: 2px;
}
/deep/.reportId input.el-input__inner {
height: 28px !important;
}
</style>

25
src/views/modules/labelSetting/label_setting.vue

@ -51,13 +51,14 @@
fixed="left"
header-align="center"
align="center"
width="200"
width="280"
:label=labels.operationLabel>
<template slot-scope="scope">
<a class="customer-a" v-if="!authEdit" @click="editLabelSettingModal(scope.row)">{{ labels.editLabel }}</a>
<a class="customer-a" v-if="!authDelete" @click="deleteLabelSettingConfirm(scope.row)">{{ labels.deleteLabel }}</a>
<a class="customer-a" v-if="!authEdit" @click="labelParameterModal(scope.row)">{{ labels.printParameterLabel }}</a>
<a class="customer-a" v-if="!authEdit" @click="labelContentModal(scope.row)">{{ labels.printContentLabel }}</a>
<a class="customer-a" v-if="!authEdit" @click="labelDrawModal(scope.row)">{{ labels.printDrawLabel }}</a>
</template>
</el-table-column>
</el-table>
@ -173,11 +174,11 @@ export default {
serialNumber: '100008001LabelLabelName',
tableId: '100008001Label',
tableName: '标签自定义列表',
columnProp: 'labelName',
columnProp: 'printDirection',
headerAlign: 'center',
align: 'center',
columnLabel: '报表文件名',
columnWidth: 220,
columnLabel: '打印方向',
columnWidth: 140,
columnHidden: false,
columnImage: false,
columnSortable: true,
@ -257,6 +258,7 @@ export default {
deleteLabel: '删除',
printParameterLabel: '打印参数',
printContentLabel: '标签内容定义',
printDrawLabel: '标签内容绘制',
sureDeleteThisRecord: '确定要删除该记录吗?',
tipsLabel: '提示',
confirmLabel: '确定',
@ -359,12 +361,23 @@ export default {
/*标签打印定义功能*/
labelContentModal(row){
let currentData = row;
//
//
this.$nextTick(() => {
this.$refs.comShowLabelContent.init(currentData);
})
},
labelDrawModal(row){
let currentData = row;
if (currentData.printDirection === '横向打印') {
this.$router.push({ name: 'labelSetting-label_draw2',
params: {labelSetting: currentData}})
} else {
this.$router.push({ name: 'labelSetting-label_draw',
params: {labelSetting: currentData}})
}
},
/* 设置选中行的参数 */
setCurrentRow(row, column, event) {
this.currentRow = JSON.parse(JSON.stringify(row))
@ -437,7 +450,7 @@ export default {
//
this.getButtonAuthData();
//
this.getMultiLanguageList();
//this.getMultiLanguageList();
}

Loading…
Cancel
Save