You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1257 lines
37 KiB

9 months ago
8 months ago
9 months ago
8 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
8 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
8 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
8 months ago
9 months ago
8 months ago
9 months ago
9 months ago
9 months ago
9 months ago
8 months ago
8 months ago
9 months ago
8 months ago
9 months ago
8 months ago
9 months ago
8 months ago
9 months ago
8 months ago
9 months ago
8 months ago
9 months ago
8 months ago
9 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
9 months ago
8 months ago
9 months ago
8 months ago
9 months ago
8 months ago
9 months ago
9 months ago
9 months ago
9 months ago
8 months ago
8 months ago
8 months ago
9 months ago
8 months ago
9 months ago
8 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
8 months ago
8 months ago
8 months ago
9 months ago
9 months ago
  1. <template>
  2. <div class="label-designer">
  3. <!-- 主要内容区域 -->
  4. <div class="main-content">
  5. <!-- 顶部工具栏区域 -->
  6. <div class="top-toolbar">
  7. <HorizontalToolbar
  8. :label-no="labelNo"
  9. :element-count="elements.length"
  10. @update:labelNo="labelNo = $event"
  11. @drag-start="handleToolDragStart"
  12. @clear-canvas="handleClearCanvas"
  13. @clear-selection="handleClearSelection"
  14. @preview="handlePreview"
  15. @save="handleSave"
  16. />
  17. </div>
  18. <!-- 画布区域 -->
  19. <div class="canvas-area">
  20. <!-- 画布信息栏 -->
  21. <div class="canvas-info-bar">
  22. <div class="canvas-info">
  23. <span class="info-item">
  24. <strong>{{ currentCanvasSize.name }}</strong>
  25. ({{ currentCanvasSize.widthMm }}×{{ currentCanvasSize.heightMm }}mm)
  26. </span>
  27. <!-- <span class="info-item">
  28. 精确: {{ currentCanvasSize.exactWidth }}×{{ currentCanvasSize.exactHeight }}px
  29. </span>-->
  30. <span class="info-item">
  31. 显示: {{ currentCanvasSize.width }}×{{ currentCanvasSize.height }}px
  32. </span>
  33. <span class="info-item" v-if="currentCanvasSize.isScaled">
  34. 缩放: {{ Math.round(currentCanvasSize.scale * 100) }}%
  35. </span>
  36. </div>
  37. <div class="canvas-middle-controls">
  38. <!-- 纸张选择 -->
  39. <div class="control-item">
  40. <label class="control-label">纸张尺寸:</label>
  41. <PaperSelector
  42. :selected-paper="selectedPaper"
  43. :orientation="orientation"
  44. :horizontal-mode="true"
  45. @paper-change="handlePaperChange"
  46. @orientation-change="handleOrientationChange"
  47. />
  48. </div>
  49. <div class="canvas-info">
  50. <span class="info-item">
  51. 显示: {{ currentCanvasSize.width }}×{{ currentCanvasSize.height }}px
  52. </span>
  53. <span class="info-item" v-if="currentCanvasSize.isScaled">
  54. 缩放: {{ Math.round(currentCanvasSize.scale * 100) }}%
  55. </span>
  56. </div>
  57. <!-- 网格设置 -->
  58. <div class="control-item">
  59. <label class="control-label">网格:</label>
  60. <div class="grid-controls">
  61. <el-checkbox
  62. v-model="showGrid"
  63. size="small"
  64. >显示</el-checkbox>
  65. <el-checkbox
  66. v-model="snapToGrid"
  67. size="small"
  68. >网格对齐</el-checkbox>
  69. <div class="grid-size-control">
  70. <span class="size-label">网格大小:</span>
  71. <el-input-number
  72. v-model="gridSize"
  73. :min="5"
  74. :max="100"
  75. :step="5"
  76. size="mini"
  77. :controls="false"
  78. style="width: 60px;"
  79. />
  80. </div>
  81. </div>
  82. </div>
  83. </div>
  84. <div class="canvas-controls">
  85. <el-select v-model="selectedDPI" @change="handleDPIChange" size="small" style="width: 120px;">
  86. <el-option
  87. v-for="option in dpiOptions"
  88. :key="option.value"
  89. :label="option.label"
  90. :value="option.value"
  91. :title="option.description"
  92. />
  93. </el-select>
  94. </div>
  95. </div>
  96. <div class="canvas-container" ref="canvasContainer">
  97. <DesignCanvas
  98. ref="canvas"
  99. :key="`canvas-${selectedPaper}-${selectedDPI}-${orientation}-${canvasUpdateKey}`"
  100. :orientation="orientation"
  101. :show-grid="showGrid"
  102. :grid-size="gridSize"
  103. :elements="elements"
  104. :selected-index="selectedIndex"
  105. :canvas-size="currentCanvasSize"
  106. @drop="handleDrop"
  107. @element-select="handleElementSelect"
  108. @element-drag="handleElementDrag"
  109. />
  110. </div>
  111. </div>
  112. </div>
  113. <!-- 右侧属性面板区域 -->
  114. <div class="right-panel">
  115. <PropertyPanel
  116. :orientation="orientation"
  117. :elements="elements"
  118. :selected-index="selectedIndex"
  119. :selected-element="selectedElement"
  120. :selected-paper="selectedPaper+''"
  121. :canvas-size="currentCanvasSize"
  122. @delete-element="handleDeleteElement"
  123. @save="handleSave"
  124. @preview="handlePreview"
  125. @data-source="handleDataSource"
  126. @element-combination="handleElementCombination"
  127. @font-changed="handleFontChanged"
  128. />
  129. </div>
  130. <!-- 数据源选择对话框 -->
  131. <DataSourceDialog
  132. :visible="dataSourceVisible"
  133. :data-keys="dataKeys"
  134. :current-text="currentElementText"
  135. :source-type="sourceType"
  136. @update:visible="dataSourceVisible = $event"
  137. @confirm="handleDataSourceConfirm"
  138. />
  139. <!-- 元素组合对话框 -->
  140. <ElementCombinationDialog
  141. :visible="elementCombinationVisible"
  142. :target-element="currentCombinationElement"
  143. :all-elements="elements"
  144. :data-keys="dataKeys"
  145. @update:visible="elementCombinationVisible = $event"
  146. @confirm="handleElementCombinationConfirm"
  147. />
  148. </div>
  149. </template>
  150. <script>
  151. import HorizontalToolbar from './components/HorizontalToolbar.vue'
  152. import DesignCanvas from './components/DesignCanvas.vue'
  153. import PropertyPanel from './components/PropertyPanel.vue'
  154. import DataSourceDialog from './components/DataSourceDialog.vue'
  155. import ElementCombinationDialog from './components/ElementCombinationDialog.vue'
  156. import PaperSelector from './components/PaperSelector.vue'
  157. import { CoordinateTransformer } from '@/utils/coordinateTransform.js'
  158. import { ZPLGenerator } from '@/utils/zplGenerator.js'
  159. import { getCanvasSize, getRecommendedGridSize } from '@/utils/paperConfig.js'
  160. import { saveZplElements, getZplElements } from '@/api/labelSetting/label_setting.js'
  161. import {getViewFieldsByLabelType} from '@/api/labelSetting/com_add_update_label.js';
  162. import { calculateCanvasConfig, calculateCanvasConfigById, getRecommendedDPI, DPI_CONFIGS } from '@/utils/canvasCalculator.js'
  163. import dynamicPaperConfig from '@/utils/paperConfigDynamic.js'
  164. import { debounce } from 'lodash'
  165. export default {
  166. name: 'LabelDesigner',
  167. components: {
  168. HorizontalToolbar,
  169. DesignCanvas,
  170. PropertyPanel,
  171. DataSourceDialog,
  172. ElementCombinationDialog,
  173. PaperSelector
  174. },
  175. props: {
  176. orientation: {
  177. type: String,
  178. default: 'portrait',
  179. validator: val => ['portrait', 'landscape'].includes(val)
  180. }
  181. },
  182. data() {
  183. return {
  184. sourceType: 'text', // 数据源类型,默认为文本
  185. labelNo: '',
  186. showGrid: true,
  187. snapToGrid: true,
  188. gridSize: 10,
  189. selectedPaper: null, // 动态纸张ID,初始为null
  190. selectedDPI: 300, // 新绘制标签默认DPI为300
  191. paperLoaded: false, // 纸张数据是否已加载
  192. elements: [],
  193. selectedIndex: -1,
  194. dataSourceVisible: false,
  195. dataKeys: [],
  196. currentElementText: '', // 当前元素的文本内容
  197. elementCombinationVisible: false,
  198. currentCombinationElement: null, // 当前要组合的元素
  199. labelSettings: {},
  200. partialVisibilityWarned: false, // 部分可见性警告状态
  201. debouncedBoundaryMessage: null, // 防抖边界消息函数
  202. containerSize: { width: 800, height: 600 }, // 画布容器尺寸
  203. canvasUpdateKey: 0 // 强制更新画布的key
  204. }
  205. },
  206. computed: {
  207. selectedElement() {
  208. return this.selectedIndex >= 0 ? this.elements[this.selectedIndex] : null
  209. },
  210. currentCanvasSize() {
  211. try {
  212. // 如果纸张数据未加载或未选择纸张,返回默认尺寸
  213. if (!this.paperLoaded || !this.selectedPaper) {
  214. return this.getDefaultCanvasSize()
  215. }
  216. // 使用新的动态画布计算功能
  217. const canvasConfig = calculateCanvasConfigById(
  218. this.selectedPaper,
  219. this.orientation,
  220. this.selectedDPI,
  221. this.containerSize
  222. )
  223. // 添加调试信息
  224. console.log('currentCanvasSize 计算:', {
  225. paperId: this.selectedPaper,
  226. orientation: this.orientation,
  227. dpi: this.selectedDPI,
  228. exactSize: `${canvasConfig.exact.width}×${canvasConfig.exact.height}px`,
  229. physicalSize: `${canvasConfig.exact.widthMm}×${canvasConfig.exact.heightMm}mm`
  230. })
  231. return {
  232. // 使用精确尺寸,不进行缩放
  233. width: canvasConfig.exact.width,
  234. height: canvasConfig.exact.height,
  235. // 精确尺寸(用于坐标转换和ZPL生成)
  236. exactWidth: canvasConfig.exact.width,
  237. exactHeight: canvasConfig.exact.height,
  238. // 物理信息
  239. name: canvasConfig.physical.name,
  240. description: canvasConfig.meta.description,
  241. // 缩放信息(现在总是1:1)
  242. scale: 1,
  243. isScaled: false,
  244. // DPI信息
  245. dpi: this.selectedDPI,
  246. // 物理尺寸(毫米)
  247. widthMm: canvasConfig.exact.widthMm,
  248. heightMm: canvasConfig.exact.heightMm,
  249. // 纸张ID
  250. paperId: this.selectedPaper,
  251. // 添加更新键确保对象变化
  252. updateKey: `${this.selectedPaper}-${this.selectedDPI}-${this.orientation}-${Date.now()}`
  253. }
  254. } catch (error) {
  255. console.error('画布尺寸计算错误:', error)
  256. // 降级到原有的计算方式
  257. return this.fallbackCanvasSize()
  258. }
  259. },
  260. // 降级的画布尺寸计算
  261. fallbackCanvasSize() {
  262. // 使用原有的getCanvasSize方法
  263. const canvasSize = getCanvasSize(this.selectedPaper, this.orientation)
  264. return {
  265. ...canvasSize,
  266. dpi: this.selectedDPI,
  267. widthMm: canvasSize.width * 25.4 / this.selectedDPI,
  268. heightMm: canvasSize.height * 25.4 / this.selectedDPI,
  269. scale: 1,
  270. isScaled: false
  271. }
  272. },
  273. // 可用的DPI选项
  274. dpiOptions() {
  275. return Object.keys(DPI_CONFIGS).map(dpi => ({
  276. value: parseInt(dpi),
  277. label: DPI_CONFIGS[dpi].name,
  278. description: DPI_CONFIGS[dpi].description
  279. }))
  280. },
  281. coordinateTransformer() {
  282. // 使用纸张ID创建坐标转换器
  283. return new CoordinateTransformer(this.orientation, null, null, this.selectedPaper)
  284. },
  285. zplGenerator() {
  286. // 使用纸张ID和DPI创建ZPL生成器
  287. return new ZPLGenerator(this.orientation, null, null, this.selectedPaper, this.selectedDPI)
  288. }
  289. },
  290. async created() {
  291. await this.initializePapers()
  292. this.initializeFromRoute()
  293. },
  294. async activated() {
  295. await this.initializePapers()
  296. this.initializeFromRoute()
  297. this.loadElements()
  298. this.updateGridSize()
  299. },
  300. watch: {
  301. selectedPaper: {
  302. handler() {
  303. this.updateGridSize()
  304. this.updateTransformers()
  305. }
  306. },
  307. selectedDPI: {
  308. handler(newDPI, oldDPI) {
  309. if (oldDPI && newDPI !== oldDPI) {
  310. console.log('DPI监听器触发:', { oldDPI, newDPI })
  311. // DPI变化时立即处理
  312. this.handleDPIChange()
  313. }
  314. }
  315. },
  316. },
  317. methods: {
  318. // 初始化纸张数据
  319. async initializePapers() {
  320. try {
  321. await dynamicPaperConfig.loadPapers()
  322. this.paperLoaded = true
  323. // 如果还没有选择纸张,设置默认纸张
  324. if (!this.selectedPaper) {
  325. const papers = dynamicPaperConfig.getActivePapers()
  326. if (papers.length > 0) {
  327. // 优先选择4×2英寸作为默认纸张
  328. const defaultPaper = papers.find(p => p.name.includes('4×2')) || papers[0]
  329. this.selectedPaper = defaultPaper.id
  330. }
  331. }
  332. console.log('纸张数据初始化完成:', dynamicPaperConfig.getAllPapers().length)
  333. } catch (error) {
  334. console.error('初始化纸张数据失败:', error)
  335. this.paperLoaded = false
  336. }
  337. },
  338. // 获取默认画布尺寸
  339. getDefaultCanvasSize() {
  340. return {
  341. width: 812,
  342. height: 406,
  343. exactWidth: 812,
  344. exactHeight: 406,
  345. name: '默认纸张',
  346. description: '4×2英寸默认尺寸',
  347. scale: 1,
  348. isScaled: false,
  349. dpi: this.selectedDPI,
  350. widthMm: 101.6,
  351. heightMm: 50.8,
  352. paperId: null,
  353. updateKey: `default-${this.selectedDPI}-${this.orientation}-${Date.now()}`
  354. }
  355. },
  356. initializeFromRoute() {
  357. const labelSetting = this.$route.params.labelSetting
  358. if (labelSetting) {
  359. this.labelNo = labelSetting.labelNo
  360. this.labelSettings = labelSetting
  361. // 根据 ReportFileList 中的纸张信息设置
  362. if (labelSetting.paperId) {
  363. // 新格式:使用纸张ID
  364. this.selectedPaper = labelSetting.paperId
  365. console.log('从路由参数设置纸张ID:', labelSetting.paperId)
  366. } else if (labelSetting.paperSize) {
  367. // 兼容旧格式:根据纸张类型查找对应的纸张ID
  368. const legacyPaper = dynamicPaperConfig.findPaperByLegacyType(labelSetting.paperSize)
  369. if (legacyPaper) {
  370. this.selectedPaper = legacyPaper.id
  371. console.log('从路由参数转换纸张类型:', labelSetting.paperSize, '-> ID:', legacyPaper.id)
  372. } else {
  373. console.warn('未找到对应的纸张类型:', labelSetting.paperSize)
  374. }
  375. }
  376. if (labelSetting.dpi) {
  377. this.selectedDPI = labelSetting.dpi
  378. console.log('从路由参数设置DPI:', labelSetting.dpi)
  379. } else {
  380. // 没有历史DPI时,确保使用默认值 300
  381. this.selectedDPI = 300
  382. console.log('使用默认DPI: 300')
  383. }
  384. // 处理打印方向,优先使用 prop 传入的值,然后是路由参数
  385. if (this.orientation && this.orientation !== 'portrait') {
  386. // 如果组件prop已经指定了方向(如从label_draw2.vue传入的landscape),使用它
  387. console.log('使用组件prop指定的打印方向:', this.orientation)
  388. } else if (labelSetting.paperOrientation) {
  389. this.orientation = labelSetting.paperOrientation
  390. console.log('从路由参数设置打印方向(paperOrientation):', labelSetting.paperOrientation)
  391. } else if (labelSetting.printDirection) {
  392. // 将中文打印方向转换为英文
  393. this.orientation = (labelSetting.printDirection === '横向打印' || labelSetting.printDirection === '横向设计') ? 'landscape' : 'portrait'
  394. console.log('从路由参数设置打印方向(printDirection):', labelSetting.printDirection, '-> orientation:', this.orientation)
  395. } else {
  396. // 没有历史打印方向时,确保使用默认值 portrait
  397. this.orientation = 'portrait'
  398. console.log('使用默认打印方向: portrait')
  399. }
  400. // 输出初始化信息
  401. console.log('标签设计器初始化完成:', {
  402. labelNo: this.labelNo,
  403. paperId: this.selectedPaper,
  404. dpi: this.selectedDPI,
  405. orientation: this.orientation,
  406. hasHistoryData: {
  407. paperId: !!labelSetting.paperId,
  408. paperSize: !!labelSetting.paperSize,
  409. dpi: !!labelSetting.dpi,
  410. orientation: !!labelSetting.paperOrientation
  411. }
  412. })
  413. } else {
  414. // 完全没有路由参数时,使用所有默认值
  415. console.log('没有路由参数,使用所有默认值:', {
  416. paperId: this.selectedPaper,
  417. dpi: this.selectedDPI,
  418. orientation: this.orientation
  419. })
  420. }
  421. },
  422. async loadElements() {
  423. if (!this.labelNo) return
  424. try {
  425. const { data } = await getZplElements({ reportId: this.labelNo })
  426. if (data.code === 200) {
  427. // 修复:对每个元素补全属性,保证响应式
  428. const defaultElement = {reportId: '',
  429. type: '', x: 0, y: 0, data: '', fontSize: 30, bold: false, newline: false, lineRows: 2,
  430. lineWidth: 200, digits: 6, step: 1, width: 100, height: 30, previewUrl: '', barcodeType: '', showContent: true, showElement: true, showSerialNumber: true,
  431. showMainSeq: false,parentSerialLabelNo:'', seqName: '', isChecked: false, decimalPlaces: '', showDecimalPlaces: false, thousandsSeparator: false, dateOffsetDays: 0,
  432. // 字体相关属性
  433. fontFamily: 'default', textAlign: 'left', letterSpacing: 0, fontItalic: false, fontUnderline: false
  434. };
  435. this.elements = (data.data || []).map(item => {
  436. const element = Object.assign({}, defaultElement, item);
  437. // 为一维码元素确保有新属性和合理的毫米默认值
  438. if (element.type === 'onecode') {
  439. if (!element.barcodeType) element.barcodeType = 'CODE128';
  440. if (element.showContent === undefined) element.showContent = true;
  441. }
  442. // 为流水号元素确保显示属性默认为true
  443. if (element.type === 'serialNumber') {
  444. if (element.showSerialNumber === undefined || element.showSerialNumber === null || element.showSerialNumber === '') {
  445. element.showSerialNumber = true;
  446. }
  447. // 如果是字符串类型,转换为布尔值
  448. if (typeof element.showSerialNumber === 'string') {
  449. element.showSerialNumber = element.showSerialNumber === 'true';
  450. }
  451. }
  452. return element;
  453. });
  454. }
  455. } catch (error) {
  456. this.$message.error('加载标签元素失败')
  457. }
  458. },
  459. handleToolDragStart(type) {
  460. // 工具栏拖拽开始,由画布处理
  461. },
  462. handleDrop(dropData) {
  463. console.log('主设计器接收到拖拽数据:', dropData) // 调试信息
  464. if (!this.labelNo) {
  465. this.$alert('标签编号不可为空!', '错误', {
  466. confirmButtonText: '确定'
  467. })
  468. return
  469. }
  470. const newElement = this.createNewElement(dropData)
  471. console.log('创建的新元素:', newElement) // 调试信息
  472. this.elements.push(newElement)
  473. // 自动选中新添加的元素
  474. this.selectedIndex = this.elements.length - 1
  475. },
  476. createNewElement({ type, x, y }) {
  477. console.log('创建新元素:', { type, x, y }) // 调试信息
  478. const baseElement = {
  479. type,
  480. x,
  481. y,
  482. data: '',
  483. fontSize: 30,
  484. bold: false,
  485. newline: false,
  486. lineRows: 2,
  487. lineWidth: 200,
  488. digits: 6,
  489. step: 1,
  490. }
  491. // 根据类型设置默认尺寸和特殊属性
  492. const sizeConfig = {
  493. text: {
  494. width: 100,
  495. height: 30,
  496. data: '',
  497. isChecked: false,
  498. // 字体相关属性
  499. fontFamily: 'default',
  500. textAlign: 'left',
  501. letterSpacing: 0,
  502. fontItalic: false,
  503. fontUnderline: false,
  504. },
  505. onecode: {
  506. width: 0.5, // 默认宽度0.5mm
  507. height: 10, // 默认高度10mm
  508. data: '',
  509. barcodeType: 'CODE128', // 默认条码类型
  510. showContent: true // 默认显示内容
  511. },
  512. qrcode: {
  513. width: 10,
  514. height: 15, // 默认尺寸15mm
  515. data: ''
  516. },
  517. pic: {
  518. width: 200, // 默认宽度200像素
  519. height: 100 // 默认高度100像素
  520. },
  521. hLine: {
  522. width: 400,
  523. height: 3
  524. },
  525. vLine: {
  526. width: 3,
  527. height: 400
  528. },
  529. serialNumber: {
  530. width: 120,
  531. height: 30,
  532. digits: 6,
  533. step: 1,
  534. data: '流水号', // 默认显示值
  535. fontSize: 30,
  536. showMainSeq: false,
  537. parentSerialLabelNo:'',
  538. reportId: this.labelNo,
  539. showSerialNumber: true // 默认显示流水号
  540. }
  541. }
  542. const config = sizeConfig[type] || { width: 100, height: 30 }
  543. const newElement = {
  544. ...baseElement,
  545. ...config
  546. }
  547. console.log('新元素配置:', newElement) // 调试信息
  548. return newElement
  549. },
  550. handleElementSelect(index) {
  551. this.selectedIndex = index
  552. },
  553. handleElementDrag({ index, x, y, boundaryStatus }) {
  554. // 网格吸附处理
  555. if (this.snapToGrid && this.gridSize > 0) {
  556. x = Math.round(x / this.gridSize) * this.gridSize
  557. y = Math.round(y / this.gridSize) * this.gridSize
  558. }
  559. // 更新元素位置
  560. this.$set(this.elements[index], 'x', x)
  561. this.$set(this.elements[index], 'y', y)
  562. // 处理边界状态反馈
  563. if (boundaryStatus) {
  564. this.handleBoundaryFeedback(boundaryStatus, this.elements[index])
  565. }
  566. },
  567. /**
  568. * 处理边界状态反馈
  569. */
  570. handleBoundaryFeedback(boundaryStatus, element) {
  571. // 只有在真正需要时才给出反馈
  572. // 1. 严格模式元素触及边界时提示
  573. if (boundaryStatus.clamped && this.isStrictElement(element.type)) {
  574. const directions = []
  575. if (boundaryStatus.atLeft) directions.push('左')
  576. if (boundaryStatus.atRight) directions.push('右')
  577. if (boundaryStatus.atTop) directions.push('上')
  578. if (boundaryStatus.atBottom) directions.push('下')
  579. if (directions.length > 0) {
  580. // 使用防抖避免频繁提示
  581. if (!this.debouncedBoundaryMessage) {
  582. this.debouncedBoundaryMessage = debounce(() => {
  583. this.$message({
  584. message: `${this.getElementTypeName(element.type)}已到达${directions.join('、')}边界`,
  585. type: 'info',
  586. duration: 1500,
  587. showClose: false
  588. })
  589. }, 1500) // 增加防抖时间
  590. }
  591. this.debouncedBoundaryMessage()
  592. }
  593. }
  594. // 2. 元素真正超出安全范围时警告
  595. if (boundaryStatus.partiallyVisible && !this.partialVisibilityWarned) {
  596. this.$message({
  597. message: '元素超出安全范围,可能影响打印效果',
  598. type: 'warning',
  599. duration: 3000
  600. })
  601. this.partialVisibilityWarned = true
  602. // 5秒后重置警告状态
  603. setTimeout(() => {
  604. this.partialVisibilityWarned = false
  605. }, 5000)
  606. }
  607. },
  608. /**
  609. * 判断是否为严格模式元素
  610. */
  611. isStrictElement(elementType) {
  612. return ['onecode', 'qrcode'].includes(elementType)
  613. },
  614. /**
  615. * 获取元素类型的中文名称
  616. */
  617. getElementTypeName(elementType) {
  618. const nameMap = {
  619. text: '文本',
  620. onecode: '一维码',
  621. qrcode: '二维码',
  622. pic: '图片',
  623. hLine: '横线',
  624. vLine: '竖线'
  625. }
  626. return nameMap[elementType] || '元素'
  627. },
  628. handleDeleteElement() {
  629. if (this.selectedIndex >= 0) {
  630. this.$confirm('确定要删除这个元素吗?', '提示', {
  631. confirmButtonText: '确定',
  632. cancelButtonText: '取消',
  633. type: 'warning'
  634. }).then(() => {
  635. this.elements.splice(this.selectedIndex, 1)
  636. this.selectedIndex = -1
  637. })
  638. }
  639. },
  640. async handleSave() {
  641. if (!this.labelNo) {
  642. this.$alert('标签编号不可为空!', '错误', {
  643. confirmButtonText: '确定'
  644. })
  645. return
  646. }
  647. const zplCode = this.zplGenerator.generate(this.elements)
  648. const saveData = {
  649. zplCode,
  650. elements: this.elements,
  651. reportId: this.labelNo,
  652. // 新格式:使用纸张ID
  653. paperId: this.selectedPaper,
  654. // 兼容性:保留原有字段
  655. paperSize: this.selectedPaper, // 现在存储的是纸张ID
  656. paperOrientation: this.orientation,
  657. dpi: this.selectedDPI,
  658. // 添加画布尺寸信息(像素)
  659. canvasWidth: this.currentCanvasSize.width,
  660. canvasHeight: this.currentCanvasSize.height,
  661. // 添加物理尺寸信息(毫米)
  662. physicalWidthMm: this.currentCanvasSize.widthMm,
  663. physicalHeightMm: this.currentCanvasSize.heightMm
  664. }
  665. try {
  666. const { data } = await saveZplElements(saveData)
  667. if (data.code === 0) {
  668. this.$message.success('保存成功!')
  669. // 输出保存的详细信息用于调试
  670. console.log('标签保存成功:', {
  671. reportId: this.labelNo,
  672. paperId: this.selectedPaper,
  673. orientation: this.orientation,
  674. dpi: this.selectedDPI,
  675. canvasSize: `${this.currentCanvasSize.width}×${this.currentCanvasSize.height}px`,
  676. physicalSize: `${this.currentCanvasSize.widthMm}×${this.currentCanvasSize.heightMm}mm`
  677. })
  678. } else {
  679. this.$message.error(data.msg)
  680. }
  681. } catch (error) {
  682. console.error('保存失败:', error)
  683. this.$message.error('保存失败')
  684. }
  685. },
  686. handlePreview() {
  687. // 预览逻辑由PropertyPanel处理
  688. },
  689. handleClearCanvas() {
  690. if (this.elements.length === 0) {
  691. this.$message.info('画布已经是空的')
  692. return
  693. }
  694. this.$confirm('确定要清空画布吗?此操作不可恢复。', '清空画布', {
  695. confirmButtonText: '确定清空',
  696. cancelButtonText: '取消',
  697. type: 'warning',
  698. dangerouslyUseHTMLString: true,
  699. message: '<div style="color: #E6A23C;"><i class="el-icon-warning"></i> 确定要清空画布吗?</div><div style="margin-top: 10px; color: #909399; font-size: 13px;">此操作将删除所有设计元素,且不可恢复。</div>'
  700. }).then(() => {
  701. this.elements = []
  702. this.selectedIndex = -1
  703. this.$message.success('画布已清空')
  704. }).catch(() => {
  705. // 用户取消操作
  706. })
  707. },
  708. handleClearSelection() {
  709. if (this.selectedIndex >= 0) {
  710. this.selectedIndex = -1
  711. this.$message.info('已取消选择')
  712. }
  713. },
  714. async handleDataSource(inData) {
  715. const response = await getViewFieldsByLabelType({
  716. labelType: this.labelSettings.labelType,
  717. site: this.$store.state.user.site
  718. });
  719. if (response.data && response.data.code === 200) {
  720. this.dataKeys = response.data.data.map(field => ({
  721. ...field,
  722. fieldDescription: field.fieldDescription || ''
  723. }));
  724. this.currentElementText = inData.data || ''
  725. this.sourceType = inData.type || 'text'
  726. this.dataSourceVisible = true
  727. } else {
  728. this.$message.error(response.data.msg || '获取字段信息失败');
  729. }
  730. },
  731. handleDataSourceConfirm(selectedKeys, sourceType) {
  732. if (this.selectedElement) {
  733. this.selectedElement.data = sourceType==='serialNumber'?selectedKeys.map(key => `#{${key}}`).join('+'):
  734. selectedKeys.map(key => `#{${key}}`).join('')
  735. }
  736. },
  737. async handleElementCombination(element) {
  738. // 获取数据源字段信息(如果还没有的话)
  739. if (!this.dataKeys.length) {
  740. try {
  741. const response = await getViewFieldsByLabelType({
  742. labelType: this.labelSettings.labelType,
  743. site: this.$store.state.user.site
  744. });
  745. if (response.data && response.data.code === 200) {
  746. this.dataKeys = response.data.data.map(field => ({
  747. ...field,
  748. fieldDescription: field.fieldDescription || ''
  749. }));
  750. }
  751. } catch (error) {
  752. console.error('获取数据源字段失败:', error);
  753. }
  754. }
  755. this.currentCombinationElement = element;
  756. this.elementCombinationVisible = true;
  757. },
  758. handleElementCombinationConfirm(combinationConfig) {
  759. if (this.currentCombinationElement) {
  760. // 根据组合模式设置元素数据
  761. switch (combinationConfig.mode) {
  762. case 'template':
  763. this.currentCombinationElement.data = combinationConfig.data;
  764. break;
  765. case 'sequence':
  766. this.currentCombinationElement.data = this.currentCombinationElement.data + combinationConfig.data;
  767. break;
  768. case 'custom':
  769. // 对于自定义模式,我们需要在后端处理时执行表达式
  770. this.currentCombinationElement.data = `CUSTOM:${combinationConfig.data}`;
  771. break;
  772. }
  773. // 保存组合配置到元素的扩展属性中
  774. this.$set(this.currentCombinationElement, 'combinationConfig', combinationConfig);
  775. this.$message.success('元素组合设置已保存');
  776. }
  777. },
  778. handlePaperChange(paperId) {
  779. this.selectedPaper = paperId
  780. // 获取纸张信息
  781. const paper = dynamicPaperConfig.getPaperById(paperId)
  782. const paperName = paper ? paper.name : `纸张ID: ${paperId}`
  783. // 获取新的画布尺寸信息
  784. const newCanvasSize = this.currentCanvasSize
  785. const orientationText = this.orientation === 'portrait' ? '纵向' : '横向'
  786. this.$message({
  787. message: `已切换到 ${paperName} (${orientationText}: ${newCanvasSize.width}×${newCanvasSize.height}px)`,
  788. type: 'success',
  789. duration: 6000
  790. })
  791. // 调试信息
  792. console.log('纸张切换:', {
  793. paperId,
  794. paperName,
  795. orientation: this.orientation,
  796. canvasSize: newCanvasSize
  797. })
  798. },
  799. handleOrientationChange(orientation) {
  800. // 更新打印方向
  801. this.orientation = orientation
  802. // 获取新的画布尺寸信息
  803. const newCanvasSize = this.currentCanvasSize
  804. this.$message({
  805. message: `已切换到${orientation === 'portrait' ? '纵向' : '横向'}打印 (${newCanvasSize.width}×${newCanvasSize.height}px)`,
  806. type: 'success',
  807. duration: 3000
  808. })
  809. // 调试信息
  810. console.log('打印方向切换:', {
  811. orientation,
  812. paper: this.selectedPaper,
  813. canvasSize: newCanvasSize
  814. })
  815. },
  816. updateGridSize() {
  817. // 根据纸张大小推荐网格尺寸
  818. const recommendedSize = getRecommendedGridSize(this.selectedPaper)
  819. if (this.gridSize === 20) { // 只在默认值时自动调整
  820. this.gridSize = recommendedSize
  821. }
  822. },
  823. handleDPIChange() {
  824. console.log('handleDPIChange 被调用, DPI:', this.selectedDPI)
  825. // 强制更新画布组件的 key,确保重新渲染
  826. this.canvasUpdateKey++
  827. // 立即更新相关计算
  828. this.updateTransformers()
  829. // 使用 $nextTick 确保 DOM 更新
  830. this.$nextTick(() => {
  831. const newCanvasSize = this.currentCanvasSize
  832. console.log('DPI变更后的画布尺寸:', {
  833. dpi: this.selectedDPI,
  834. paper: this.selectedPaper,
  835. orientation: this.orientation,
  836. canvasSize: {
  837. display: `${newCanvasSize.width}×${newCanvasSize.height}px`,
  838. exact: `${newCanvasSize.exactWidth}×${newCanvasSize.exactHeight}px`,
  839. physical: `${newCanvasSize.widthMm}×${newCanvasSize.heightMm}mm`
  840. }
  841. })
  842. this.$message({
  843. message: `DPI已切换到 ${this.selectedDPI} (精确尺寸: ${newCanvasSize.exactWidth}×${newCanvasSize.exactHeight}px)`,
  844. type: 'success',
  845. duration: 3000
  846. })
  847. })
  848. },
  849. updateContainerSize() {
  850. // 动态检测画布容器尺寸
  851. if (this.$refs.canvasContainer) {
  852. const rect = this.$refs.canvasContainer.getBoundingClientRect()
  853. this.containerSize = {
  854. width: Math.max(rect.width - 40, 400), // 减去padding
  855. height: Math.max(rect.height - 40, 300)
  856. }
  857. }
  858. },
  859. updateTransformers() {
  860. // 更新坐标转换器和ZPL生成器的纸张ID
  861. if (this.coordinateTransformer && this.coordinateTransformer.updatePaperId) {
  862. this.coordinateTransformer.updatePaperId(this.selectedPaper)
  863. }
  864. if (this.zplGenerator && this.zplGenerator.updatePaperId) {
  865. this.zplGenerator.updatePaperId(this.selectedPaper)
  866. }
  867. },
  868. // 处理字体变化
  869. handleFontChanged(fontData) {
  870. console.log('LabelDesigner收到字体变化事件:', fontData)
  871. if (fontData && fontData.elementId && this.selectedElement && this.selectedElement.id === fontData.elementId) {
  872. // 确保字体值正确设置
  873. this.$set(this.selectedElement, 'fontFamily', fontData.fontFamily)
  874. console.log('字体已更新:', this.selectedElement.fontFamily)
  875. // 强制更新画布
  876. this.canvasUpdateKey++
  877. // 触发ZPL预览更新
  878. this.$nextTick(() => {
  879. this.$emit('font-updated', fontData)
  880. })
  881. }
  882. }
  883. },
  884. mounted() {
  885. // 初始化容器尺寸检测
  886. this.updateContainerSize()
  887. // 监听窗口大小变化
  888. window.addEventListener('resize', this.updateContainerSize)
  889. // 使用ResizeObserver监听容器尺寸变化(如果支持)
  890. if (window.ResizeObserver && this.$refs.canvasContainer) {
  891. this.resizeObserver = new ResizeObserver(() => {
  892. this.updateContainerSize()
  893. })
  894. this.resizeObserver.observe(this.$refs.canvasContainer)
  895. }
  896. },
  897. beforeDestroy() {
  898. // 清理事件监听器
  899. window.removeEventListener('resize', this.updateContainerSize)
  900. if (this.resizeObserver) {
  901. this.resizeObserver.disconnect()
  902. }
  903. }
  904. }
  905. </script>
  906. <style scoped>
  907. .label-designer {
  908. display: flex;
  909. height: 100vh;
  910. overflow: hidden;
  911. background: #f5f7fa;
  912. }
  913. /* 主要内容区域 - 占据大部分空间 */
  914. .main-content {
  915. flex: 1;
  916. display: flex;
  917. flex-direction: column;
  918. overflow: hidden;
  919. }
  920. /* 顶部工具栏区域 - 自适应高度 */
  921. .top-toolbar {
  922. flex-shrink: 0;
  923. background: #fff;
  924. border-bottom: 1px solid #e4e7ed;
  925. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
  926. }
  927. /* 画布区域 - 占据剩余空间 */
  928. .canvas-area {
  929. flex: 1;
  930. display: flex;
  931. flex-direction: column;
  932. overflow: hidden;
  933. background: #fafafa;
  934. }
  935. /* 画布信息栏 */
  936. .canvas-info-bar {
  937. display: flex;
  938. justify-content: space-between;
  939. align-items: center;
  940. padding: 12px 20px;
  941. background: #fff;
  942. border-bottom: 1px solid #e4e7ed;
  943. box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
  944. gap: 20px;
  945. }
  946. .canvas-info {
  947. display: flex;
  948. gap: 20px;
  949. align-items: center;
  950. flex-shrink: 0;
  951. }
  952. .info-item {
  953. font-size: 13px;
  954. color: #606266;
  955. white-space: nowrap;
  956. }
  957. .info-item strong {
  958. color: #303133;
  959. font-weight: 600;
  960. }
  961. .canvas-middle-controls {
  962. display: flex;
  963. gap: 30px;
  964. align-items: center;
  965. flex: 1;
  966. justify-content: left;
  967. }
  968. .control-item {
  969. display: flex;
  970. align-items: center;
  971. gap: 8px;
  972. white-space: nowrap;
  973. }
  974. .control-label {
  975. font-size: 13px;
  976. color: #606266;
  977. font-weight: 500;
  978. min-width: fit-content;
  979. }
  980. .grid-controls {
  981. display: flex;
  982. align-items: center;
  983. gap: 15px;
  984. }
  985. .grid-size-control {
  986. display: flex;
  987. align-items: center;
  988. gap: 5px;
  989. }
  990. .size-label {
  991. font-size: 12px;
  992. color: #909399;
  993. }
  994. .canvas-controls {
  995. display: flex;
  996. gap: 10px;
  997. align-items: center;
  998. flex-shrink: 0;
  999. }
  1000. /* 画布容器 - 支持滚动的原始尺寸画布 */
  1001. .canvas-container {
  1002. flex: 1;
  1003. padding: 20px;
  1004. overflow: auto;
  1005. position: relative;
  1006. background: #fafafa;
  1007. /* 确保滚动条样式美观 */
  1008. scrollbar-width: thin;
  1009. scrollbar-color: #c1c1c1 #f1f1f1;
  1010. }
  1011. .canvas-container::-webkit-scrollbar {
  1012. width: 8px;
  1013. height: 8px;
  1014. }
  1015. .canvas-container::-webkit-scrollbar-track {
  1016. background: #f1f1f1;
  1017. border-radius: 4px;
  1018. }
  1019. .canvas-container::-webkit-scrollbar-thumb {
  1020. background: #c1c1c1;
  1021. border-radius: 4px;
  1022. }
  1023. .canvas-container::-webkit-scrollbar-thumb:hover {
  1024. background: #a8a8a8;
  1025. }
  1026. /* 右侧属性面板区域 - 固定宽度 */
  1027. .right-panel {
  1028. width: 400px;
  1029. flex-shrink: 0;
  1030. background: #fff;
  1031. border-left: 1px solid #e4e7ed;
  1032. overflow-y: auto;
  1033. overflow-x: hidden;
  1034. }
  1035. /* 响应式设计 - 小屏幕适配 */
  1036. @media (max-width: 1200px) {
  1037. .top-toolbar {
  1038. height: 100px;
  1039. }
  1040. .right-panel {
  1041. width: 300px;
  1042. }
  1043. .canvas-info {
  1044. gap: 15px;
  1045. }
  1046. .info-item {
  1047. font-size: 12px;
  1048. }
  1049. .canvas-middle-controls {
  1050. gap: 20px;
  1051. }
  1052. }
  1053. @media (max-width: 1024px) {
  1054. .top-toolbar {
  1055. height: 80px;
  1056. }
  1057. .right-panel {
  1058. width: 280px;
  1059. }
  1060. .canvas-info-bar {
  1061. flex-direction: column;
  1062. gap: 10px;
  1063. align-items: flex-start;
  1064. padding: 10px 15px;
  1065. }
  1066. .canvas-info {
  1067. flex-wrap: wrap;
  1068. gap: 10px;
  1069. }
  1070. .canvas-middle-controls {
  1071. flex-direction: row;
  1072. justify-content: flex-start;
  1073. gap: 15px;
  1074. width: 100%;
  1075. }
  1076. .grid-controls {
  1077. gap: 10px;
  1078. }
  1079. }
  1080. @media (max-width: 768px) {
  1081. .label-designer {
  1082. flex-direction: column;
  1083. }
  1084. .main-content {
  1085. flex: 1;
  1086. }
  1087. .right-panel {
  1088. width: 100%;
  1089. height: 300px;
  1090. border-left: none;
  1091. border-top: 1px solid #e4e7ed;
  1092. }
  1093. .top-toolbar {
  1094. height: 60px;
  1095. }
  1096. .canvas-middle-controls {
  1097. flex-direction: column;
  1098. align-items: flex-start;
  1099. gap: 10px;
  1100. }
  1101. .control-item {
  1102. width: 100%;
  1103. justify-content: space-between;
  1104. }
  1105. .grid-controls {
  1106. flex-wrap: wrap;
  1107. gap: 8px;
  1108. }
  1109. }
  1110. </style>