|
|
<template> <div class="label-designer"> <!-- 主要内容区域 --> <div class="main-content"> <!-- 顶部工具栏区域 --> <div class="top-toolbar"> <HorizontalToolbar :label-no="labelNo" @update:labelNo="labelNo = $event" @drag-start="handleToolDragStart" /> </div>
<!-- 画布区域 --> <div class="canvas-area"> <!-- 画布信息栏 --> <div class="canvas-info-bar"> <div class="canvas-info"> <span class="info-item"> <strong>{{ currentCanvasSize.name }}</strong> ({{ currentCanvasSize.widthMm }}×{{ currentCanvasSize.heightMm }}mm) </span><!-- <span class="info-item"> 精确: {{ currentCanvasSize.exactWidth }}×{{ currentCanvasSize.exactHeight }}px </span>--> <span class="info-item"> 显示: {{ currentCanvasSize.width }}×{{ currentCanvasSize.height }}px </span> <span class="info-item" v-if="currentCanvasSize.isScaled"> 缩放: {{ Math.round(currentCanvasSize.scale * 100) }}% </span> </div>
<div class="canvas-middle-controls"> <!-- 纸张选择 --> <div class="control-item"> <label class="control-label">纸张尺寸:</label> <PaperSelector :selected-paper="selectedPaper" :orientation="orientation" :horizontal-mode="true" @paper-change="handlePaperChange" @orientation-change="handleOrientationChange" /> </div> <div class="canvas-info"> <span class="info-item"> 显示: {{ currentCanvasSize.width }}×{{ currentCanvasSize.height }}px </span> <span class="info-item" v-if="currentCanvasSize.isScaled"> 缩放: {{ Math.round(currentCanvasSize.scale * 100) }}% </span> </div> <!-- 网格设置 --> <div class="control-item"> <label class="control-label">网格:</label> <div class="grid-controls"> <el-checkbox v-model="showGrid" size="small" >显示</el-checkbox>
<el-checkbox v-model="snapToGrid" size="small" >网格对齐</el-checkbox>
<div class="grid-size-control"> <span class="size-label">网格大小:</span> <el-input-number v-model="gridSize" :min="5" :max="100" :step="5" size="mini" :controls="false" style="width: 60px;" /> </div> </div> </div> </div>
<div class="canvas-controls"> <el-select v-model="selectedDPI" @change="handleDPIChange" size="small" style="width: 120px;"> <el-option v-for="option in dpiOptions" :key="option.value" :label="option.label" :value="option.value" :title="option.description" /> </el-select> </div> </div>
<div class="canvas-container" ref="canvasContainer"> <DesignCanvas ref="canvas" :key="`canvas-${selectedPaper}-${selectedDPI}-${orientation}-${canvasUpdateKey}`" :orientation="orientation" :show-grid="showGrid" :grid-size="gridSize" :elements="elements" :selected-index="selectedIndex" :canvas-size="currentCanvasSize" @drop="handleDrop" @element-select="handleElementSelect" @element-drag="handleElementDrag" /> </div> </div> </div>
<!-- 右侧属性面板区域 --> <div class="right-panel"> <PropertyPanel :orientation="orientation" :elements="elements" :selected-index="selectedIndex" :selected-element="selectedElement" :selected-paper="selectedPaper+''" :canvas-size="currentCanvasSize" @delete-element="handleDeleteElement" @save="handleSave" @preview="handlePreview" @data-source="handleDataSource" /> </div>
<!-- 数据源选择对话框 --> <DataSourceDialog :visible="dataSourceVisible" :data-keys="dataKeys" :current-text="currentElementText" :source-type="sourceType" @update:visible="dataSourceVisible = $event" @confirm="handleDataSourceConfirm" /> </div></template>
<script>import HorizontalToolbar from './components/HorizontalToolbar.vue'import DesignCanvas from './components/DesignCanvas.vue'import PropertyPanel from './components/PropertyPanel.vue'import DataSourceDialog from './components/DataSourceDialog.vue'import PaperSelector from './components/PaperSelector.vue'import { CoordinateTransformer } from '@/utils/coordinateTransform.js'import { ZPLGenerator } from '@/utils/zplGenerator.js'import { getCanvasSize, getRecommendedGridSize } from '@/utils/paperConfig.js'import { saveZplElements, getZplElements } from '@/api/labelSetting/label_setting.js'import {getViewFieldsByLabelType} from '@/api/labelSetting/com_add_update_label.js';import { calculateCanvasConfig, calculateCanvasConfigById, getRecommendedDPI, DPI_CONFIGS } from '@/utils/canvasCalculator.js'import dynamicPaperConfig from '@/utils/paperConfigDynamic.js'import { debounce } from 'lodash'
export default { name: 'LabelDesigner', components: { HorizontalToolbar, DesignCanvas, PropertyPanel, DataSourceDialog, PaperSelector }, props: { orientation: { type: String, default: 'portrait', validator: val => ['portrait', 'landscape'].includes(val) } }, data() { return { sourceType: 'text', // 数据源类型,默认为文本
labelNo: '', showGrid: true, snapToGrid: true, gridSize: 10, selectedPaper: null, // 动态纸张ID,初始为null
selectedDPI: 300, // 新绘制标签默认DPI为300
paperLoaded: false, // 纸张数据是否已加载
elements: [], selectedIndex: -1, dataSourceVisible: false, dataKeys: [], currentElementText: '', // 当前元素的文本内容
labelSettings: {}, partialVisibilityWarned: false, // 部分可见性警告状态
debouncedBoundaryMessage: null, // 防抖边界消息函数
containerSize: { width: 800, height: 600 }, // 画布容器尺寸
canvasUpdateKey: 0 // 强制更新画布的key
} }, computed: { selectedElement() { return this.selectedIndex >= 0 ? this.elements[this.selectedIndex] : null }, currentCanvasSize() { try { // 如果纸张数据未加载或未选择纸张,返回默认尺寸
if (!this.paperLoaded || !this.selectedPaper) { return this.getDefaultCanvasSize() }
// 使用新的动态画布计算功能
const canvasConfig = calculateCanvasConfigById( this.selectedPaper, this.orientation, this.selectedDPI, this.containerSize )
// 添加调试信息
console.log('currentCanvasSize 计算:', { paperId: this.selectedPaper, orientation: this.orientation, dpi: this.selectedDPI, exactSize: `${canvasConfig.exact.width}×${canvasConfig.exact.height}px`, physicalSize: `${canvasConfig.exact.widthMm}×${canvasConfig.exact.heightMm}mm` })
return { // 使用精确尺寸,不进行缩放
width: canvasConfig.exact.width, height: canvasConfig.exact.height,
// 精确尺寸(用于坐标转换和ZPL生成)
exactWidth: canvasConfig.exact.width, exactHeight: canvasConfig.exact.height,
// 物理信息
name: canvasConfig.physical.name, description: canvasConfig.meta.description,
// 缩放信息(现在总是1:1)
scale: 1, isScaled: false,
// DPI信息
dpi: this.selectedDPI,
// 物理尺寸(毫米)
widthMm: canvasConfig.exact.widthMm, heightMm: canvasConfig.exact.heightMm,
// 纸张ID
paperId: this.selectedPaper,
// 添加更新键确保对象变化
updateKey: `${this.selectedPaper}-${this.selectedDPI}-${this.orientation}-${Date.now()}` } } catch (error) { console.error('画布尺寸计算错误:', error) // 降级到原有的计算方式
return this.fallbackCanvasSize() } },
// 降级的画布尺寸计算
fallbackCanvasSize() { // 使用原有的getCanvasSize方法
const canvasSize = getCanvasSize(this.selectedPaper, this.orientation) return { ...canvasSize, dpi: this.selectedDPI, widthMm: canvasSize.width * 25.4 / this.selectedDPI, heightMm: canvasSize.height * 25.4 / this.selectedDPI, scale: 1, isScaled: false } },
// 可用的DPI选项
dpiOptions() { return Object.keys(DPI_CONFIGS).map(dpi => ({ value: parseInt(dpi), label: DPI_CONFIGS[dpi].name, description: DPI_CONFIGS[dpi].description })) }, coordinateTransformer() { // 使用纸张ID创建坐标转换器
return new CoordinateTransformer(this.orientation, null, null, this.selectedPaper) }, zplGenerator() { // 使用纸张ID和DPI创建ZPL生成器
return new ZPLGenerator(this.orientation, null, null, this.selectedPaper, this.selectedDPI) } }, async created() { await this.initializePapers() this.initializeFromRoute() }, async activated() { await this.initializePapers() this.initializeFromRoute() this.loadElements() this.updateGridSize() }, watch: { selectedPaper: { handler() { this.updateGridSize() this.updateTransformers() } }, selectedDPI: { handler(newDPI, oldDPI) { if (oldDPI && newDPI !== oldDPI) { console.log('DPI监听器触发:', { oldDPI, newDPI }) // DPI变化时立即处理
this.handleDPIChange() } } },
}, methods: { // 初始化纸张数据
async initializePapers() { try { await dynamicPaperConfig.loadPapers() this.paperLoaded = true
// 如果还没有选择纸张,设置默认纸张
if (!this.selectedPaper) { const papers = dynamicPaperConfig.getActivePapers() if (papers.length > 0) { // 优先选择4×2英寸作为默认纸张
const defaultPaper = papers.find(p => p.name.includes('4×2')) || papers[0] this.selectedPaper = defaultPaper.id } }
console.log('纸张数据初始化完成:', dynamicPaperConfig.getAllPapers().length) } catch (error) { console.error('初始化纸张数据失败:', error) this.paperLoaded = false } },
// 获取默认画布尺寸
getDefaultCanvasSize() { return { width: 812, height: 406, exactWidth: 812, exactHeight: 406, name: '默认纸张', description: '4×2英寸默认尺寸', scale: 1, isScaled: false, dpi: this.selectedDPI, widthMm: 101.6, heightMm: 50.8, paperId: null, updateKey: `default-${this.selectedDPI}-${this.orientation}-${Date.now()}` } },
initializeFromRoute() { const labelSetting = this.$route.params.labelSetting if (labelSetting) { this.labelNo = labelSetting.labelNo this.labelSettings = labelSetting
// 根据 ReportFileList 中的纸张信息设置
if (labelSetting.paperId) { // 新格式:使用纸张ID
this.selectedPaper = labelSetting.paperId console.log('从路由参数设置纸张ID:', labelSetting.paperId) } else if (labelSetting.paperSize) { // 兼容旧格式:根据纸张类型查找对应的纸张ID
const legacyPaper = dynamicPaperConfig.findPaperByLegacyType(labelSetting.paperSize) if (legacyPaper) { this.selectedPaper = legacyPaper.id console.log('从路由参数转换纸张类型:', labelSetting.paperSize, '-> ID:', legacyPaper.id) } else { console.warn('未找到对应的纸张类型:', labelSetting.paperSize) } }
if (labelSetting.dpi) { this.selectedDPI = labelSetting.dpi console.log('从路由参数设置DPI:', labelSetting.dpi) } else { // 没有历史DPI时,确保使用默认值 300
this.selectedDPI = 300 console.log('使用默认DPI: 300') }
if (labelSetting.paperOrientation) { this.orientation = labelSetting.paperOrientation console.log('从路由参数设置打印方向:', labelSetting.paperOrientation) } else { // 没有历史打印方向时,确保使用默认值 portrait
this.orientation = 'portrait' console.log('使用默认打印方向: portrait') }
// 输出初始化信息
console.log('标签设计器初始化完成:', { labelNo: this.labelNo, paperId: this.selectedPaper, dpi: this.selectedDPI, orientation: this.orientation, hasHistoryData: { paperId: !!labelSetting.paperId, paperSize: !!labelSetting.paperSize, dpi: !!labelSetting.dpi, orientation: !!labelSetting.paperOrientation } }) } else { // 完全没有路由参数时,使用所有默认值
console.log('没有路由参数,使用所有默认值:', { paperId: this.selectedPaper, dpi: this.selectedDPI, orientation: this.orientation }) } },
async loadElements() { if (!this.labelNo) return
try { const { data } = await getZplElements({ reportId: this.labelNo }) if (data.code === 200) { // 修复:对每个元素补全属性,保证响应式
const defaultElement = { type: '', x: 0, y: 0, data: '', fontSize: 30, bold: false, newline: false, lineRows: 2, lineWidth: 200, digits: 6, step: 1, width: 100, height: 30, previewUrl: '', barcodeType: '', showContent: true,showPic:true, showMainSeq:false,seqName:'',isChecked:false,decimalPlaces:'',showDecimalPlaces:false,thousandsSeparator:false }; this.elements = (data.data || []).map(item => { const element = Object.assign({}, defaultElement, item); // 为一维码元素确保有新属性和合理的毫米默认值
if (element.type === 'onecode') { if (!element.barcodeType) element.barcodeType = 'CODE128'; if (element.showContent === undefined) element.showContent = true; } return element; }); } } catch (error) { this.$message.error('加载标签元素失败') } },
handleToolDragStart(type) { // 工具栏拖拽开始,由画布处理
},
handleDrop(dropData) { console.log('主设计器接收到拖拽数据:', dropData) // 调试信息
if (!this.labelNo) { this.$alert('标签编号不可为空!', '错误', { confirmButtonText: '确定' }) return }
const newElement = this.createNewElement(dropData) console.log('创建的新元素:', newElement) // 调试信息
this.elements.push(newElement)
// 自动选中新添加的元素
this.selectedIndex = this.elements.length - 1 },
createNewElement({ type, x, y }) { console.log('创建新元素:', { type, x, y }) // 调试信息
const baseElement = { type, x, y, data: '', fontSize: 30, bold: false, newline: false, lineRows: 2, lineWidth: 200, digits: 6, step: 1, }
// 根据类型设置默认尺寸和特殊属性
const sizeConfig = { text: { width: 100, height: 30, data: '', isChecked:false, }, onecode: { width: 2, // 默认宽度2mm
height: 15, // 默认高度15mm
data: '', barcodeType: 'CODE128', // 默认条码类型
showContent: true // 默认显示内容
}, qrcode: { width: 10, height: 10, // 默认尺寸10mm
data: '' }, pic: { width: 100, height: 100 }, hLine: { width: 400, height: 3 }, vLine: { width: 3, height: 400 }, serialNumber: { width: 120, height: 30, digits: 6, step: 1, data: '流水号', // 默认显示值
fontSize: 30, showMainSeq: false, } }
const config = sizeConfig[type] || { width: 100, height: 30 }
const newElement = { ...baseElement, ...config }
console.log('新元素配置:', newElement) // 调试信息
return newElement },
handleElementSelect(index) { this.selectedIndex = index },
handleElementDrag({ index, x, y, boundaryStatus }) { // 网格吸附处理
if (this.snapToGrid && this.gridSize > 0) { x = Math.round(x / this.gridSize) * this.gridSize y = Math.round(y / this.gridSize) * this.gridSize }
// 更新元素位置
this.$set(this.elements[index], 'x', x) this.$set(this.elements[index], 'y', y)
// 处理边界状态反馈
if (boundaryStatus) { this.handleBoundaryFeedback(boundaryStatus, this.elements[index]) } },
/** * 处理边界状态反馈 */ handleBoundaryFeedback(boundaryStatus, element) { // 只有在真正需要时才给出反馈
// 1. 严格模式元素触及边界时提示
if (boundaryStatus.clamped && this.isStrictElement(element.type)) { const directions = [] if (boundaryStatus.atLeft) directions.push('左') if (boundaryStatus.atRight) directions.push('右') if (boundaryStatus.atTop) directions.push('上') if (boundaryStatus.atBottom) directions.push('下')
if (directions.length > 0) { // 使用防抖避免频繁提示
if (!this.debouncedBoundaryMessage) { this.debouncedBoundaryMessage = debounce(() => { this.$message({ message: `${this.getElementTypeName(element.type)}已到达${directions.join('、')}边界`, type: 'info', duration: 1500, showClose: false }) }, 1500) // 增加防抖时间
}
this.debouncedBoundaryMessage() } }
// 2. 元素真正超出安全范围时警告
if (boundaryStatus.partiallyVisible && !this.partialVisibilityWarned) { this.$message({ message: '元素超出安全范围,可能影响打印效果', type: 'warning', duration: 3000 }) this.partialVisibilityWarned = true
// 5秒后重置警告状态
setTimeout(() => { this.partialVisibilityWarned = false }, 5000) } },
/** * 判断是否为严格模式元素 */ isStrictElement(elementType) { return ['onecode', 'qrcode'].includes(elementType) },
/** * 获取元素类型的中文名称 */ getElementTypeName(elementType) { const nameMap = { text: '文本', onecode: '一维码', qrcode: '二维码', pic: '图片', hLine: '横线', vLine: '竖线' } return nameMap[elementType] || '元素' },
handleDeleteElement() { if (this.selectedIndex >= 0) { this.$confirm('确定要删除这个元素吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { this.elements.splice(this.selectedIndex, 1) this.selectedIndex = -1 }) } },
async handleSave() { if (!this.labelNo) { this.$alert('标签编号不可为空!', '错误', { confirmButtonText: '确定' }) return }
const zplCode = this.zplGenerator.generate(this.elements) const saveData = { zplCode, elements: this.elements, reportId: this.labelNo, // 新格式:使用纸张ID
paperId: this.selectedPaper, // 兼容性:保留原有字段
paperSize: this.selectedPaper, // 现在存储的是纸张ID
paperOrientation: this.orientation, dpi: this.selectedDPI, // 添加画布尺寸信息(像素)
canvasWidth: this.currentCanvasSize.width, canvasHeight: this.currentCanvasSize.height, // 添加物理尺寸信息(毫米)
physicalWidthMm: this.currentCanvasSize.widthMm, physicalHeightMm: this.currentCanvasSize.heightMm }
try { const { data } = await saveZplElements(saveData) if (data.code === 0) { this.$message.success('保存成功!')
// 输出保存的详细信息用于调试
console.log('标签保存成功:', { reportId: this.labelNo, paperId: this.selectedPaper, orientation: this.orientation, dpi: this.selectedDPI, canvasSize: `${this.currentCanvasSize.width}×${this.currentCanvasSize.height}px`, physicalSize: `${this.currentCanvasSize.widthMm}×${this.currentCanvasSize.heightMm}mm` }) } else { this.$message.error(data.msg) } } catch (error) { console.error('保存失败:', error) this.$message.error('保存失败') } },
handlePreview() { // 预览逻辑由PropertyPanel处理
},
async handleDataSource(inData) { 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 || '' })); this.currentElementText = inData.data || '' this.sourceType = inData.type || 'text' this.dataSourceVisible = true } else { this.$message.error(response.data.msg || '获取字段信息失败'); } },
handleDataSourceConfirm(selectedKeys, sourceType) { if (this.selectedElement) { this.selectedElement.data = sourceType==='serialNumber'?selectedKeys.map(key => `#{${key}}`).join('+'): selectedKeys.map(key => `#{${key}}`).join('') } },
handlePaperChange(paperId) { this.selectedPaper = paperId
// 获取纸张信息
const paper = dynamicPaperConfig.getPaperById(paperId) const paperName = paper ? paper.name : `纸张ID: ${paperId}`
// 获取新的画布尺寸信息
const newCanvasSize = this.currentCanvasSize const orientationText = this.orientation === 'portrait' ? '纵向' : '横向'
this.$message({ message: `已切换到 ${paperName} (${orientationText}: ${newCanvasSize.width}×${newCanvasSize.height}px)`, type: 'success', duration: 6000 })
// 调试信息
console.log('纸张切换:', { paperId, paperName, orientation: this.orientation, canvasSize: newCanvasSize }) },
handleOrientationChange(orientation) { // 更新打印方向
this.orientation = orientation
// 获取新的画布尺寸信息
const newCanvasSize = this.currentCanvasSize
this.$message({ message: `已切换到${orientation === 'portrait' ? '纵向' : '横向'}打印 (${newCanvasSize.width}×${newCanvasSize.height}px)`, type: 'success', duration: 3000 })
// 调试信息
console.log('打印方向切换:', { orientation, paper: this.selectedPaper, canvasSize: newCanvasSize }) },
updateGridSize() { // 根据纸张大小推荐网格尺寸
const recommendedSize = getRecommendedGridSize(this.selectedPaper) if (this.gridSize === 20) { // 只在默认值时自动调整
this.gridSize = recommendedSize } },
handleDPIChange() { console.log('handleDPIChange 被调用, DPI:', this.selectedDPI)
// 强制更新画布组件的 key,确保重新渲染
this.canvasUpdateKey++
// 立即更新相关计算
this.updateTransformers()
// 使用 $nextTick 确保 DOM 更新
this.$nextTick(() => { const newCanvasSize = this.currentCanvasSize
console.log('DPI变更后的画布尺寸:', { dpi: this.selectedDPI, paper: this.selectedPaper, orientation: this.orientation, canvasSize: { display: `${newCanvasSize.width}×${newCanvasSize.height}px`, exact: `${newCanvasSize.exactWidth}×${newCanvasSize.exactHeight}px`, physical: `${newCanvasSize.widthMm}×${newCanvasSize.heightMm}mm` } })
this.$message({ message: `DPI已切换到 ${this.selectedDPI} (精确尺寸: ${newCanvasSize.exactWidth}×${newCanvasSize.exactHeight}px)`, type: 'success', duration: 3000 }) }) },
updateContainerSize() { // 动态检测画布容器尺寸
if (this.$refs.canvasContainer) { const rect = this.$refs.canvasContainer.getBoundingClientRect() this.containerSize = { width: Math.max(rect.width - 40, 400), // 减去padding
height: Math.max(rect.height - 40, 300) } } },
updateTransformers() { // 更新坐标转换器和ZPL生成器的纸张ID
if (this.coordinateTransformer && this.coordinateTransformer.updatePaperId) { this.coordinateTransformer.updatePaperId(this.selectedPaper) } if (this.zplGenerator && this.zplGenerator.updatePaperId) { this.zplGenerator.updatePaperId(this.selectedPaper) } } },
mounted() { // 初始化容器尺寸检测
this.updateContainerSize()
// 监听窗口大小变化
window.addEventListener('resize', this.updateContainerSize)
// 使用ResizeObserver监听容器尺寸变化(如果支持)
if (window.ResizeObserver && this.$refs.canvasContainer) { this.resizeObserver = new ResizeObserver(() => { this.updateContainerSize() }) this.resizeObserver.observe(this.$refs.canvasContainer) } },
beforeDestroy() { // 清理事件监听器
window.removeEventListener('resize', this.updateContainerSize)
if (this.resizeObserver) { this.resizeObserver.disconnect() } }}</script>
<style scoped>.label-designer { display: flex; height: 100vh; overflow: hidden; background: #f5f7fa;}
/* 主要内容区域 - 占据大部分空间 */.main-content { flex: 1; display: flex; flex-direction: column; overflow: hidden;}
/* 顶部工具栏区域 - 自适应高度 */.top-toolbar { flex-shrink: 0; background: #fff; border-bottom: 1px solid #e4e7ed; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);}
/* 画布区域 - 占据剩余空间 */.canvas-area { flex: 1; display: flex; flex-direction: column; overflow: hidden; background: #fafafa;}
/* 画布信息栏 */.canvas-info-bar { display: flex; justify-content: space-between; align-items: center; padding: 12px 20px; background: #fff; border-bottom: 1px solid #e4e7ed; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05); gap: 20px;}
.canvas-info { display: flex; gap: 20px; align-items: center; flex-shrink: 0;}
.info-item { font-size: 13px; color: #606266; white-space: nowrap;}
.info-item strong { color: #303133; font-weight: 600;}
.canvas-middle-controls { display: flex; gap: 30px; align-items: center; flex: 1; justify-content: left;}
.control-item { display: flex; align-items: center; gap: 8px; white-space: nowrap;}
.control-label { font-size: 13px; color: #606266; font-weight: 500; min-width: fit-content;}
.grid-controls { display: flex; align-items: center; gap: 15px;}
.grid-size-control { display: flex; align-items: center; gap: 5px;}
.size-label { font-size: 12px; color: #909399;}
.canvas-controls { display: flex; gap: 10px; align-items: center; flex-shrink: 0;}
/* 画布容器 - 支持滚动的原始尺寸画布 */.canvas-container { flex: 1; padding: 20px; overflow: auto; position: relative; background: #fafafa;
/* 确保滚动条样式美观 */ scrollbar-width: thin; scrollbar-color: #c1c1c1 #f1f1f1;}
.canvas-container::-webkit-scrollbar { width: 8px; height: 8px;}
.canvas-container::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px;}
.canvas-container::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 4px;}
.canvas-container::-webkit-scrollbar-thumb:hover { background: #a8a8a8;}
/* 右侧属性面板区域 - 固定宽度 */.right-panel { width: 400px; flex-shrink: 0; background: #fff; border-left: 1px solid #e4e7ed; overflow-y: auto; overflow-x: hidden;}
/* 响应式设计 - 小屏幕适配 */@media (max-width: 1200px) { .top-toolbar { height: 100px; }
.right-panel { width: 300px; }
.canvas-info { gap: 15px; }
.info-item { font-size: 12px; }
.canvas-middle-controls { gap: 20px; }}
@media (max-width: 1024px) { .top-toolbar { height: 80px; }
.right-panel { width: 280px; }
.canvas-info-bar { flex-direction: column; gap: 10px; align-items: flex-start; padding: 10px 15px; }
.canvas-info { flex-wrap: wrap; gap: 10px; }
.canvas-middle-controls { flex-direction: row; justify-content: flex-start; gap: 15px; width: 100%; }
.grid-controls { gap: 10px; }}
@media (max-width: 768px) { .label-designer { flex-direction: column; }
.main-content { flex: 1; }
.right-panel { width: 100%; height: 300px; border-left: none; border-top: 1px solid #e4e7ed; }
.top-toolbar { height: 60px; }
.canvas-middle-controls { flex-direction: column; align-items: flex-start; gap: 10px; }
.control-item { width: 100%; justify-content: space-between; }
.grid-controls { flex-wrap: wrap; gap: 8px; }}</style>
|