Browse Source

界面设定的内容需要按特定格式组合到条码、二维码中

master
han\hanst 5 months ago
parent
commit
6edecaa451
  1. 7
      src/utils/zplGenerator.js
  2. 62
      src/views/modules/labelSetting/LabelDesigner.vue
  3. 721
      src/views/modules/labelSetting/components/ElementCombinationDialog.vue
  4. 12
      src/views/modules/labelSetting/components/PropertyForm.vue
  5. 3
      src/views/modules/labelSetting/components/PropertyPanel.vue

7
src/utils/zplGenerator.js

@ -159,11 +159,8 @@ export class ZPLGenerator {
const showContent = element.showContent !== false // 默认显示内容 const showContent = element.showContent !== false // 默认显示内容
// 将毫米转换为ZPL单位 // 将毫米转换为ZPL单位
// 宽度:ZPL的^BY宽度参数控制窄条宽度倍数 (范围1-10)
// 根据ZPL规范,这个参数不是直接的物理尺寸,而是相对倍数
// 使用经验公式:倍数 ≈ 宽度(mm) * 1.5,确保在1-10范围内
const widthMM = parseFloat(element.width) || 2
const width = Math.max(1, Math.min(10, Math.round(widthMM * 1.5)))
// 宽度:毫米转换为ZPL宽度倍数 (范围1-10)
const width = Math.min(10, Math.round((element.width || 0.33) * this.dpi / 25.4)) // 0.33mm≈13mil 常用
// 高度:毫米转换为点数,使用精确的DPI转换公式 // 高度:毫米转换为点数,使用精确的DPI转换公式
const height = Math.max(1, Math.round((element.height || 15) * this.dpi / 25.4)) const height = Math.max(1, Math.round((element.height || 15) * this.dpi / 25.4))

62
src/views/modules/labelSetting/LabelDesigner.vue

@ -125,6 +125,7 @@
@save="handleSave" @save="handleSave"
@preview="handlePreview" @preview="handlePreview"
@data-source="handleDataSource" @data-source="handleDataSource"
@element-combination="handleElementCombination"
/> />
</div> </div>
@ -137,6 +138,16 @@
@update:visible="dataSourceVisible = $event" @update:visible="dataSourceVisible = $event"
@confirm="handleDataSourceConfirm" @confirm="handleDataSourceConfirm"
/> />
<!-- 元素组合对话框 -->
<ElementCombinationDialog
:visible="elementCombinationVisible"
:target-element="currentCombinationElement"
:all-elements="elements"
:data-keys="dataKeys"
@update:visible="elementCombinationVisible = $event"
@confirm="handleElementCombinationConfirm"
/>
</div> </div>
</template> </template>
@ -145,6 +156,7 @@ import HorizontalToolbar from './components/HorizontalToolbar.vue'
import DesignCanvas from './components/DesignCanvas.vue' import DesignCanvas from './components/DesignCanvas.vue'
import PropertyPanel from './components/PropertyPanel.vue' import PropertyPanel from './components/PropertyPanel.vue'
import DataSourceDialog from './components/DataSourceDialog.vue' import DataSourceDialog from './components/DataSourceDialog.vue'
import ElementCombinationDialog from './components/ElementCombinationDialog.vue'
import PaperSelector from './components/PaperSelector.vue' import PaperSelector from './components/PaperSelector.vue'
import { CoordinateTransformer } from '@/utils/coordinateTransform.js' import { CoordinateTransformer } from '@/utils/coordinateTransform.js'
import { ZPLGenerator } from '@/utils/zplGenerator.js' import { ZPLGenerator } from '@/utils/zplGenerator.js'
@ -162,6 +174,7 @@ export default {
DesignCanvas, DesignCanvas,
PropertyPanel, PropertyPanel,
DataSourceDialog, DataSourceDialog,
ElementCombinationDialog,
PaperSelector PaperSelector
}, },
props: { props: {
@ -187,6 +200,8 @@ export default {
dataSourceVisible: false, dataSourceVisible: false,
dataKeys: [], dataKeys: [],
currentElementText: '', // currentElementText: '', //
elementCombinationVisible: false,
currentCombinationElement: null, //
labelSettings: {}, labelSettings: {},
partialVisibilityWarned: false, // partialVisibilityWarned: false, //
debouncedBoundaryMessage: null, // debouncedBoundaryMessage: null, //
@ -729,6 +744,53 @@ export default {
} }
}, },
async handleElementCombination(element) {
//
if (!this.dataKeys.length) {
try {
const response = await getViewFieldsByLabelType({
labelType: this.labelSettings.labelType,
site: this.$store.state.user.site
});
if (response.data && response.data.code === 200) {
this.dataKeys = response.data.data.map(field => ({
...field,
fieldDescription: field.fieldDescription || ''
}));
}
} catch (error) {
console.error('获取数据源字段失败:', error);
}
}
this.currentCombinationElement = element;
this.elementCombinationVisible = true;
},
handleElementCombinationConfirm(combinationConfig) {
if (this.currentCombinationElement) {
//
switch (combinationConfig.mode) {
case 'template':
this.currentCombinationElement.data = combinationConfig.data;
break;
case 'sequence':
this.currentCombinationElement.data = this.currentCombinationElement.data + combinationConfig.data;
break;
case 'custom':
//
this.currentCombinationElement.data = `CUSTOM:${combinationConfig.data}`;
break;
}
//
this.$set(this.currentCombinationElement, 'combinationConfig', combinationConfig);
this.$message.success('元素组合设置已保存');
}
},
handlePaperChange(paperId) { handlePaperChange(paperId) {
this.selectedPaper = paperId this.selectedPaper = paperId

721
src/views/modules/labelSetting/components/ElementCombinationDialog.vue

@ -0,0 +1,721 @@
<template>
<el-dialog
title="元素内容组合设置"
:visible="visible"
:close-on-click-modal="false"
width="900px"
top="3vh"
custom-class="element-combination-dialog"
:before-close="handleClose"
@close="handleClose"
>
<div class="combination-content">
<!-- 目标元素信息 -->
<div class="target-element-info">
<h4>目标元素{{ targetElement.type === 'onecode' ? '一维码' : '二维码' }}</h4>
<p>当前数据{{ targetElement.data || '(空)' }}</p>
</div>
<!-- 预览区域 - 移到顶部 -->
<div class="preview-section">
<h4 class="preview-title">预览结果</h4>
<div class="preview-area">
<el-input
:value="previewResult"
readonly
placeholder="组合结果预览"
/>
</div>
</div>
<el-divider></el-divider>
<!-- 组合规则设置 -->
<div class="combination-rules">
<el-form label-position="top">
<!-- 顺序拼接模式 -->
<div class="sequence-mode">
<div class="sequence-container">
<!-- 已选择的元素列表 -->
<div class="selected-section">
<h5 class="section-title">已选择的元素</h5>
<div class="selected-elements-container">
<div v-if="selectedElements.length === 0" class="empty-state">
<i class="el-icon-plus"></i>
<p>请从右侧选择要组合的元素</p>
</div>
<div
v-for="(element, index) in selectedElements"
:key="element.id || index"
class="selected-element-card"
>
<div class="element-info">
<div class="element-order">{{ index + 1 }}</div>
<i :class="getElementIcon(element.type)" class="element-icon"></i>
<span class="element-name">{{ getElementDisplayName(element) }}</span>
</div>
<div class="element-actions">
<el-button
size="mini"
icon="el-icon-arrow-up"
@click="moveElementUp(index)"
:disabled="index === 0"
class="action-btn up-btn"
title="上移"
></el-button>
<el-button
size="mini"
icon="el-icon-arrow-down"
@click="moveElementDown(index)"
:disabled="index === selectedElements.length - 1"
class="action-btn down-btn"
title="下移"
></el-button>
<el-button
size="mini"
icon="el-icon-delete"
@click="removeElement(element)"
class="action-btn remove-btn"
title="移除"
></el-button>
</div>
</div>
</div>
</div>
<!-- 可用元素列表 -->
<div class="available-section">
<h5 class="section-title">可用元素</h5>
<div class="available-elements-container">
<div v-if="availableElements.length === 0" class="no-elements">
<p>暂无可用元素</p>
</div>
<div
v-for="element in availableElements"
:key="element.id"
class="available-element-card"
@click="addElement(element)"
>
<i :class="getElementIcon(element.type)" class="element-icon"></i>
<span class="element-name">{{ getElementDisplayName(element) }}</span>
<i class="el-icon-plus add-icon"></i>
</div>
</div>
</div>
</div>
<el-form-item label="分隔符设置" class="separator-setting">
<div class="separator-input-group">
<el-input
v-model="separator"
placeholder="如:- 或 _ 或留空"
style="width: 200px;"
/>
<span class="separator-hint">元素间的分隔符</span>
</div>
</el-form-item>
</div>
</el-form>
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleConfirm">确定</el-button>
</div>
</el-dialog>
</template>
<script>
export default {
name: 'ElementCombinationDialog',
props: {
visible: Boolean,
targetElement: {
type: Object,
default: () => ({})
},
allElements: {
type: Array,
default: () => []
},
dataKeys: {
type: Array,
default: () => []
}
},
emits: ['update:visible', 'confirm'],
data() {
return {
selectedElements: [],
separator: '-'
}
},
computed: {
availableElements() {
//
const combinableTypes = ['text', 'onecode', 'qrcode', 'serialNumber']
return this.allElements.filter(element =>
element !== this.targetElement &&
!this.selectedElements.find(selected => selected.id === element.id) &&
combinableTypes.includes(element.type)
)
},
previewResult() {
try {
return this.generateSequencePreview()
} catch (error) {
return '预览错误:' + error.message
}
}
},
watch: {
visible(newVal) {
if (newVal) {
this.initializeDialog()
}
}
},
methods: {
initializeDialog() {
//
this.resetToDefaults()
},
resetToDefaults() {
this.selectedElements = []
this.separator = '-'
},
addElement(element) {
// ID
const elementWithId = {
...element,
id: element.id || `element_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
}
this.selectedElements.push(elementWithId)
},
removeElement(element) {
const index = this.selectedElements.findIndex(e => e.id === element.id)
if (index > -1) {
this.selectedElements.splice(index, 1)
}
},
//
moveElementUp(index) {
if (index > 0) {
const element = this.selectedElements[index]
this.selectedElements.splice(index, 1)
this.selectedElements.splice(index - 1, 0, element)
}
},
//
moveElementDown(index) {
if (index < this.selectedElements.length - 1) {
const element = this.selectedElements[index]
this.selectedElements.splice(index, 1)
this.selectedElements.splice(index + 1, 0, element)
}
},
getElementDisplayName(element) {
const typeNames = {
text: '文本',
onecode: '一维码',
qrcode: '二维码',
pic: '图片',
serialNumber: '流水号'
}
const typeName = typeNames[element.type] || element.type
const content = element.data ? `(${element.data.substring(0, 40)}${element.data.length > 40 ? '...' : ''})` : ''
return `${typeName}${content}`
},
//
getElementFullName(element) {
const typeNames = {
text: '文本',
onecode: '一维码',
qrcode: '二维码',
pic: '图片',
serialNumber: '流水号'
}
const typeName = typeNames[element.type] || element.type
const content = element.data ? `(${element.data})` : ''
return `${typeName}${content}`
},
getElementIcon(type) {
const icons = {
text: 'el-icon-edit',
onecode: 'el-icon-menu',
qrcode: 'el-icon-menu',
pic: 'el-icon-picture',
serialNumber: 'el-icon-sort'
}
return icons[type] || 'el-icon-document'
},
generateSequencePreview() {
if (!this.selectedElements.length) return ''
const parts = this.selectedElements.map(element =>
element.data || `[${this.getElementDisplayName(element)}]`
)
return parts.join(this.separator || '')
},
handleClose() {
this.$emit('update:visible', false)
},
handleConfirm() {
const combinationData = this.generateSequenceFormat()
const combinationConfig = {
mode: 'sequence',
data: combinationData,
selectedElements: this.selectedElements.map(e => e.id),
separator: this.separator
}
this.$emit('confirm', combinationConfig)
this.handleClose()
},
generateSequenceFormat() {
//
const parts = this.selectedElements.map(element => {
// 使
return `{${this.getElementFullName(element)}}`
})
return parts.join(this.separator || '')
}
}
}
</script>
<style scoped>
.element-combination-dialog {
border-radius: 12px;
}
.combination-content {
max-height: calc(85vh - 120px);
overflow-y: auto;
padding-right: 5px;
display: flex;
flex-direction: column;
}
.target-element-info {
background: #f5f7fa;
padding: 15px;
border-radius: 8px;
border: 1px solid #e4e7ed;
}
.target-element-info h4 {
margin: 0 0 8px 0;
color: #409eff;
font-size: 16px;
font-weight: 600;
}
.target-element-info p {
margin: 0;
color: #666;
font-size: 14px;
line-height: 1.5;
}
/* 预览区域样式 */
.preview-section {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 6px;
padding: 12px;
margin: 12px 0;
}
.preview-title {
margin: 0 0 8px 0;
color: #495057;
font-size: 14px;
font-weight: 500;
}
.combination-rules h4 {
margin: 0 0 15px 0;
color: #333;
font-size: 16px;
font-weight: 600;
}
/* 顺序拼接模式样式 */
.sequence-mode {
margin-top: 15px;
}
.sequence-container {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.selected-section,
.available-section {
flex: 1;
min-width: 0;
}
.section-title {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 600;
color: #333;
padding-bottom: 8px;
border-bottom: 2px solid #e4e7ed;
}
/* 已选择元素区域 */
.selected-elements-container {
border: 2px dashed #d9ecff;
border-radius: 8px;
min-height: 230px;
max-height: 250px;
padding: 12px;
background: #f8fbff;
overflow-y: auto;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 126px;
color: #999;
text-align: center;
}
.empty-state i {
font-size: 32px;
margin-bottom: 8px;
color: #d3d4d6;
}
.empty-state p {
margin: 0;
font-size: 14px;
}
.selected-element-card {
display: flex;
align-items: center;
justify-content: space-between;
background: #fff;
border: 1px solid #e1f3d8;
border-radius: 6px;
padding: 12px;
margin-bottom: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.selected-element-card:hover {
border-color: #95de64;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.element-info {
display: flex;
align-items: center;
flex: 1;
}
.element-order {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background: #52c41a;
color: #fff;
border-radius: 50%;
font-size: 12px;
font-weight: bold;
margin-right: 10px;
}
.element-icon {
margin-right: 8px;
color: #52c41a;
font-size: 16px;
}
.element-name {
font-size: 14px;
color: #333;
font-weight: 500;
}
.element-actions {
display: flex;
align-items: center;
gap: 4px;
}
.action-btn {
padding: 4px;
min-width: 24px;
height: 24px;
border-radius: 4px;
}
.up-btn {
color: #409eff;
border-color: #409eff;
background: #fff;
}
.up-btn:hover:not(:disabled) {
background: #409eff;
color: #fff;
}
.down-btn {
color: #409eff;
border-color: #409eff;
background: #fff;
}
.down-btn:hover:not(:disabled) {
background: #409eff;
color: #fff;
}
.remove-btn {
color: #f56c6c;
border-color: #f56c6c;
background: #fff;
}
.remove-btn:hover {
background: #f56c6c;
color: #fff;
}
.action-btn:disabled {
color: #c0c4cc;
border-color: #e4e7ed;
background: #f5f7fa;
cursor: not-allowed;
}
/* 可用元素区域 */
.available-elements-container {
border: 1px solid #e4e7ed;
border-radius: 8px;
min-height: 150px;
max-height: 230px;
overflow-y: auto;
padding: 12px;
background: #fafafa;
}
.no-elements {
display: flex;
align-items: center;
justify-content: center;
height: 126px;
color: #999;
text-align: center;
}
.no-elements p {
margin: 0;
font-size: 14px;
}
.available-element-card {
display: flex;
align-items: center;
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 6px;
padding: 10px 12px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.available-element-card:hover {
background: #e6f7ff;
border-color: #91d5ff;
transform: translateX(4px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.available-element-card .element-icon {
margin-right: 8px;
color: #409eff;
font-size: 16px;
}
.available-element-card .element-name {
flex: 1;
font-size: 14px;
color: #333;
font-weight: 500;
}
.add-icon {
color: #52c41a;
font-size: 16px;
opacity: 0;
transition: opacity 0.3s ease;
}
.available-element-card:hover .add-icon {
opacity: 1;
}
/* 分隔符设置 */
.separator-setting {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #e4e7ed;
}
.separator-input-group {
display: flex;
align-items: center;
gap: 12px;
}
.separator-hint {
color: #666;
font-size: 13px;
}
.preview-area {
background: #fff;
border-radius: 4px;
}
.preview-area .el-input {
background: #fff;
}
.preview-area .el-input__inner {
border: 1px solid #ddd;
background: #fff;
color: #333;
font-size: 14px;
}
.dialog-footer {
text-align: right;
padding-top: 15px;
border-top: 1px solid #e4e7ed;
}
.dialog-footer .el-button {
margin-left: 10px;
padding: 8px 20px;
}
/* 表单项间距优化 */
.el-form-item {
margin-bottom: 20px;
}
.el-form-item:last-child {
margin-bottom: 0;
}
/* 滚动条样式 */
.combination-content::-webkit-scrollbar,
.available-elements::-webkit-scrollbar {
width: 6px;
}
.combination-content::-webkit-scrollbar-track,
.available-elements::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.combination-content::-webkit-scrollbar-thumb,
.available-elements::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.combination-content::-webkit-scrollbar-thumb:hover,
.available-elements::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 响应式设计 */
@media (max-width: 768px) {
.sequence-container {
flex-direction: column;
gap: 15px;
}
.selected-elements-container,
.available-elements-container {
min-height: 120px;
max-height: 150px;
}
.selected-element-card {
padding: 8px 10px;
}
.element-order {
width: 20px;
height: 20px;
font-size: 11px;
margin-right: 8px;
}
.element-name {
font-size: 13px;
}
.action-btn {
padding: 2px;
min-width: 20px;
height: 20px;
}
.available-element-card {
padding: 8px 10px;
}
.separator-input-group {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
}
</style>

12
src/views/modules/labelSetting/components/PropertyForm.vue

@ -239,6 +239,9 @@
<el-button type="primary" size="mini" @click="$emit('data-source', element)"> <el-button type="primary" size="mini" @click="$emit('data-source', element)">
数据源 数据源
</el-button> </el-button>
<el-button type="success" size="mini" @click="$emit('element-combination', element)">
元素组合
</el-button>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="不显示规则(如:XXX=N)" class="form-item-half"> <el-form-item label="不显示规则(如:XXX=N)" class="form-item-half">
@ -301,6 +304,9 @@
<el-button type="primary" size="mini" @click="$emit('data-source', element)"> <el-button type="primary" size="mini" @click="$emit('data-source', element)">
数据源 数据源
</el-button> </el-button>
<el-button type="success" size="mini" @click="$emit('element-combination', element)">
元素组合
</el-button>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="不显示规则(如:XXX=N)" class="form-item-half"> <el-form-item label="不显示规则(如:XXX=N)" class="form-item-half">
@ -391,7 +397,7 @@
<!-- 流水号属性 --> <!-- 流水号属性 -->
<div v-else-if="element.type === 'serialNumber'" class="form-section"> <div v-else-if="element.type === 'serialNumber'" class="form-section">
<el-form label-position="top" size="small"> <el-form label-position="top" size="small">
<div class="form-row">
<!-- <div class="form-row">
<el-form-item label="名称" class="form-item-half"> <el-form-item label="名称" class="form-item-half">
<el-input v-model="element.seqName" controls-position="right" size="mini"/> <el-input v-model="element.seqName" controls-position="right" size="mini"/>
</el-form-item> </el-form-item>
@ -401,7 +407,7 @@
<el-radio :label="false"></el-radio> <el-radio :label="false"></el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
</div>
</div>-->
<div class="form-row" v-if="!element.showMainSeq"> <div class="form-row" v-if="!element.showMainSeq">
<el-form-item label="位数" class="form-item-half"> <el-form-item label="位数" class="form-item-half">
<el-input <el-input
@ -469,7 +475,7 @@ export default {
required: true required: true
} }
}, },
emits: ['data-source', 'image-upload'],
emits: ['data-source', 'image-upload', 'element-combination'],
/*组件*/ /*组件*/
components: { components: {
comShowLabelSerialInfo,/*标签内容流水号信息的組件*/ comShowLabelSerialInfo,/*标签内容流水号信息的組件*/

3
src/views/modules/labelSetting/components/PropertyPanel.vue

@ -18,6 +18,7 @@
<PropertyForm <PropertyForm
:element="selectedElement" :element="selectedElement"
@data-source="(currentText) => $emit('data-source', currentText)" @data-source="(currentText) => $emit('data-source', currentText)"
@element-combination="(element) => $emit('element-combination', element)"
@image-upload="handleImageUpload" @image-upload="handleImageUpload"
/> />
</div> </div>
@ -77,7 +78,7 @@ export default {
default: () => ({ width: 472, height: 315 }) default: () => ({ width: 472, height: 315 })
} }
}, },
emits: ['delete-element', 'save', 'preview', 'data-source'],
emits: ['delete-element', 'save', 'preview', 'data-source', 'element-combination'],
methods: { methods: {
handleImageUpload(imageData) { handleImageUpload(imageData) {
if (this.selectedElement && this.selectedElement.type === 'pic') { if (this.selectedElement && this.selectedElement.type === 'pic') {

Loading…
Cancel
Save