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.

1113 lines
31 KiB

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