|
@@ -0,0 +1,1530 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="template-design-container">
|
|
|
|
|
+ <!-- 左侧工具栏 -->
|
|
|
|
|
+ <div class="toolbar">
|
|
|
|
|
+ <!-- 标签页切换 -->
|
|
|
|
|
+ <el-tabs v-model="activeTab" type="card" @tab-click="handleTabClick">
|
|
|
|
|
+ <el-tab-pane label="模版设计" name="design">
|
|
|
|
|
+ <el-upload
|
|
|
|
|
+ :show-file-list="false"
|
|
|
|
|
+ :before-upload="beforeUpload"
|
|
|
|
|
+ :http-request="handleUpload"
|
|
|
|
|
+ accept="image/*"
|
|
|
|
|
+ multiple
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-button type="primary" :icon="Upload" style="width: 100%; margin-bottom: 8px;">
|
|
|
|
|
+ 上传素材图
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </el-upload>
|
|
|
|
|
+
|
|
|
|
|
+ <el-button type="success" :icon="Plus" style="width: 100%;" @click="addTextLayer">
|
|
|
|
|
+ 添加文字
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+
|
|
|
|
|
+ <el-divider />
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 画布尺寸设置 -->
|
|
|
|
|
+ <div class="canvas-settings">
|
|
|
|
|
+ <h4>画布尺寸</h4>
|
|
|
|
|
+ <div class="property-item">
|
|
|
|
|
+ <span>比例:</span>
|
|
|
|
|
+ <el-select v-model="canvasRatio" size="small" style="flex: 1;" @change="handleCanvasRatioChange">
|
|
|
|
|
+ <el-option label="自定义" value="custom" />
|
|
|
|
|
+ <el-option label="1:1 (正方形)" value="1:1" />
|
|
|
|
|
+ <el-option label="4:3 (横屏)" value="4:3" />
|
|
|
|
|
+ <el-option label="3:4 (竖屏)" value="3:4" />
|
|
|
|
|
+ <el-option label="16:9 (宽屏)" value="16:9" />
|
|
|
|
|
+ <el-option label="9:16 (手机)" value="9:16" />
|
|
|
|
|
+ <el-option label="3:2 (照片)" value="3:2" />
|
|
|
|
|
+ <el-option label="2:3 (竖照片)" value="2:3" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="property-item">
|
|
|
|
|
+ <span>宽度:</span>
|
|
|
|
|
+ <el-input-number v-model="canvasWidth" size="small" :step="10" :min="100" :max="2000" @change="handleCanvasSizeChange" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="property-item">
|
|
|
|
|
+ <span>高度:</span>
|
|
|
|
|
+ <el-input-number v-model="canvasHeight" size="small" :step="10" :min="100" :max="2000" @change="handleCanvasSizeChange" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <el-divider />
|
|
|
|
|
+
|
|
|
|
|
+ <div class="layer-info" v-if="selectedLayer">
|
|
|
|
|
+ <h4>图层属性</h4>
|
|
|
|
|
+ <div class="property-item">
|
|
|
|
|
+ <span>X:</span>
|
|
|
|
|
+ <el-input-number v-model="selectedLayer.x" size="small" :step="1" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="property-item">
|
|
|
|
|
+ <span>Y:</span>
|
|
|
|
|
+ <el-input-number v-model="selectedLayer.y" size="small" :step="1" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="property-item">
|
|
|
|
|
+ <span>宽度:</span>
|
|
|
|
|
+ <el-input-number
|
|
|
|
|
+ v-model="selectedLayer.width"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ :step="1"
|
|
|
|
|
+ @change="handleWidthChange"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="property-item">
|
|
|
|
|
+ <span>高度:</span>
|
|
|
|
|
+ <el-input-number
|
|
|
|
|
+ v-model="selectedLayer.height"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ :step="1"
|
|
|
|
|
+ @change="handleHeightChange"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="property-item">
|
|
|
|
|
+ <span>旋转:</span>
|
|
|
|
|
+ <el-input-number v-model="selectedLayer.rotation" size="small" :step="1" :min="-360" :max="360" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="property-item">
|
|
|
|
|
+ <span>透明度:</span>
|
|
|
|
|
+ <el-slider v-model="selectedLayer.opacity" :min="0" :max="100" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 文字图层特有属性 -->
|
|
|
|
|
+ <template v-if="selectedLayer.type === 'text'">
|
|
|
|
|
+ <el-divider />
|
|
|
|
|
+ <h4>文字属性</h4>
|
|
|
|
|
+ <div class="property-item">
|
|
|
|
|
+ <span>内容:</span>
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="selectedLayer.text"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ style="flex: 1;"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="property-item">
|
|
|
|
|
+ <span>字体:</span>
|
|
|
|
|
+ <el-select v-model="selectedLayer.fontFamily" size="small" style="flex: 1;">
|
|
|
|
|
+ <el-option label="Arial" value="Arial" />
|
|
|
|
|
+ <el-option label="宋体" value="SimSun" />
|
|
|
|
|
+ <el-option label="黑体" value="SimHei" />
|
|
|
|
|
+ <el-option label="微软雅黑" value="Microsoft YaHei" />
|
|
|
|
|
+ <el-option label="楷体" value="KaiTi" />
|
|
|
|
|
+ <el-option label="Times New Roman" value="Times New Roman" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="property-item">
|
|
|
|
|
+ <span>字号:</span>
|
|
|
|
|
+ <el-input-number v-model="selectedLayer.fontSize" size="small" :min="8" :max="200" :step="1" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="property-item">
|
|
|
|
|
+ <span>颜色:</span>
|
|
|
|
|
+ <el-color-picker v-model="selectedLayer.color" size="small" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="property-item">
|
|
|
|
|
+ <span>背景:</span>
|
|
|
|
|
+ <el-color-picker v-model="selectedLayer.backgroundColor" size="small" show-alpha />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="property-item">
|
|
|
|
|
+ <span>对齐:</span>
|
|
|
|
|
+ <el-radio-group v-model="selectedLayer.textAlign" size="small">
|
|
|
|
|
+ <el-radio-button label="left">左</el-radio-button>
|
|
|
|
|
+ <el-radio-button label="center">中</el-radio-button>
|
|
|
|
|
+ <el-radio-button label="right">右</el-radio-button>
|
|
|
|
|
+ </el-radio-group>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="property-item">
|
|
|
|
|
+ <span>加粗:</span>
|
|
|
|
|
+ <el-switch v-model="selectedLayer.fontWeight" active-value="bold" inactive-value="normal" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="property-item">
|
|
|
|
|
+ <span>斜体:</span>
|
|
|
|
|
+ <el-switch v-model="selectedLayer.fontStyle" active-value="italic" inactive-value="normal" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="property-item">
|
|
|
|
|
+ <span>下划线:</span>
|
|
|
|
|
+ <el-switch v-model="selectedLayer.textDecoration" active-value="underline" inactive-value="none" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="property-item">
|
|
|
|
|
+ <span>行高:</span>
|
|
|
|
|
+ <el-slider v-model="selectedLayer.lineHeight" :min="0.5" :max="3" :step="0.1" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="property-item">
|
|
|
|
|
+ <span>字距:</span>
|
|
|
|
|
+ <el-slider v-model="selectedLayer.letterSpacing" :min="-5" :max="20" :step="1" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-tab-pane>
|
|
|
|
|
+
|
|
|
|
|
+ <el-tab-pane label="素材选择" name="material" :label-class="'material-tab'">
|
|
|
|
|
+
|
|
|
|
|
+ <div class="materials-list-full">
|
|
|
|
|
+ <el-skeleton v-if="materialsLoading" :rows="5" animated />
|
|
|
|
|
+ <div v-else-if="materials.length === 0" class="empty-materials">
|
|
|
|
|
+ 暂无素材
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-else
|
|
|
|
|
+ class="material-item-full"
|
|
|
|
|
+ v-for="material in materials"
|
|
|
|
|
+ :key="material.id"
|
|
|
|
|
+ @click="addMaterialToCanvas(material)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <img :src="material.material_url" :alt="material.id" />
|
|
|
|
|
+ <div class="material-overlay">
|
|
|
|
|
+ <el-icon><Plus /></el-icon>
|
|
|
|
|
+ <span>添加</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-tab-pane>
|
|
|
|
|
+ </el-tabs>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 中间画布区域 -->
|
|
|
|
|
+ <div class="canvas-area">
|
|
|
|
|
+ <div class="canvas-wrapper">
|
|
|
|
|
+ <div
|
|
|
|
|
+ ref="canvasRef"
|
|
|
|
|
+ class="canvas"
|
|
|
|
|
+ :style="{
|
|
|
|
|
+ width: canvasWidth + 'px',
|
|
|
|
|
+ height: canvasHeight + 'px'
|
|
|
|
|
+ }"
|
|
|
|
|
+ @mousedown="handleCanvasMouseDown"
|
|
|
|
|
+ @mousemove="handleCanvasMouseMove"
|
|
|
|
|
+ @mouseup="handleCanvasMouseUp"
|
|
|
|
|
+ @wheel="handleCanvasWheel"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="(layer, index) in layers"
|
|
|
|
|
+ :key="layer.id"
|
|
|
|
|
+ class="layer"
|
|
|
|
|
+ :class="{
|
|
|
|
|
+ 'selected': selectedLayerId === layer.id,
|
|
|
|
|
+ 'text-layer': layer.type === 'text'
|
|
|
|
|
+ }"
|
|
|
|
|
+ :style="getLayerStyle(layer)"
|
|
|
|
|
+ @mousedown.stop="handleLayerMouseDown($event, layer)"
|
|
|
|
|
+ @dblclick.stop="handleLayerDblClick($event, layer)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <!-- 图片图层 -->
|
|
|
|
|
+ <template v-if="layer.type !== 'text'">
|
|
|
|
|
+ <img :src="layer.url" :alt="layer.name" draggable="false" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 文字图层 -->
|
|
|
|
|
+ <template v-else>
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="text-content"
|
|
|
|
|
+ :style="getTextStyle(layer)"
|
|
|
|
|
+ contenteditable="false"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ layer.text }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 选中状态的调整手柄 -->
|
|
|
|
|
+ <template v-if="selectedLayerId === layer.id">
|
|
|
|
|
+ <div class="resize-handle nw" @mousedown.stop="startResize($event, 'nw')"></div>
|
|
|
|
|
+ <div class="resize-handle ne" @mousedown.stop="startResize($event, 'ne')"></div>
|
|
|
|
|
+ <div class="resize-handle sw" @mousedown.stop="startResize($event, 'sw')"></div>
|
|
|
|
|
+ <div class="resize-handle se" @mousedown.stop="startResize($event, 'se')"></div>
|
|
|
|
|
+ <div class="rotate-handle" @mousedown.stop="startRotate($event)"></div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 右侧图层面板 -->
|
|
|
|
|
+ <div class="layer-panel">
|
|
|
|
|
+ <h3>图层</h3>
|
|
|
|
|
+ <div class="layer-actions">
|
|
|
|
|
+ <el-button size="small" @click="moveLayerUp" :disabled="!canMoveUp">
|
|
|
|
|
+ <el-icon><ArrowUp /></el-icon>
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ <el-button size="small" @click="moveLayerDown" :disabled="!canMoveDown">
|
|
|
|
|
+ <el-icon><ArrowDown /></el-icon>
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ <el-button size="small" type="danger" @click="deleteLayer" :disabled="!selectedLayerId">
|
|
|
|
|
+ <el-icon><Delete /></el-icon>
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <el-divider />
|
|
|
|
|
+
|
|
|
|
|
+ <div class="layer-list" ref="layerListRef">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="(layer, index) in reversedLayers"
|
|
|
|
|
+ :key="layer.id"
|
|
|
|
|
+ class="layer-item"
|
|
|
|
|
+ :class="{
|
|
|
|
|
+ 'selected': selectedLayerId === layer.id,
|
|
|
|
|
+ 'dragging': dragState.draggingId === layer.id,
|
|
|
|
|
+ 'drag-over': dragState.dragOverId === layer.id,
|
|
|
|
|
+ 'locked': layer.locked
|
|
|
|
|
+ }"
|
|
|
|
|
+ :draggable="true"
|
|
|
|
|
+ @click="selectLayer(layer.id)"
|
|
|
|
|
+ @dragstart="handleDragStart($event, layer)"
|
|
|
|
|
+ @dragover="handleDragOver($event, layer)"
|
|
|
|
|
+ @drop="handleDrop($event, layer)"
|
|
|
|
|
+ @dragend="handleDragEnd"
|
|
|
|
|
+ @dragenter="handleDragEnter($event, layer)"
|
|
|
|
|
+ @dragleave="handleDragLeave"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-icon class="drag-handle">
|
|
|
|
|
+ <Rank />
|
|
|
|
|
+ </el-icon>
|
|
|
|
|
+ <el-icon class="visibility-icon" @click.stop="toggleLayerVisibility(layer.id)">
|
|
|
|
|
+ <View v-if="layer.visible" />
|
|
|
|
|
+ <Hide v-else />
|
|
|
|
|
+ </el-icon>
|
|
|
|
|
+ <el-icon class="lock-icon" @click.stop="toggleLayerLock(layer.id)">
|
|
|
|
|
+ <Lock v-if="layer.locked" />
|
|
|
|
|
+ <Unlock v-else />
|
|
|
|
|
+ </el-icon>
|
|
|
|
|
+ <template v-if="layer.type === 'text'">
|
|
|
|
|
+ <div class="layer-thumbnail text-thumbnail">
|
|
|
|
|
+ <el-icon><Document /></el-icon>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template v-else>
|
|
|
|
|
+ <img :src="layer.url" class="layer-thumbnail" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <span class="layer-name">{{ layer.name }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-if="layers.length === 0" class="empty-tip">
|
|
|
|
|
+ 暂无图层,请上传素材
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup>
|
|
|
|
|
+import { ref, computed, reactive, onMounted } from 'vue'
|
|
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
|
|
+import { Pointer, Rank, ArrowUp, ArrowDown, Delete, View, Hide, Lock, Unlock, Plus, Document, Picture, Refresh, Upload } from '@element-plus/icons-vue'
|
|
|
|
|
+import { Material_List } from '@/api/mes/job'
|
|
|
|
|
+
|
|
|
|
|
+const canvasRef = ref(null)
|
|
|
|
|
+const layerListRef = ref(null)
|
|
|
|
|
+const currentTool = ref('select')
|
|
|
|
|
+const activeTab = ref('design') // 'design' 或 'material'
|
|
|
|
|
+const canvasWidth = ref(600)
|
|
|
|
|
+const canvasHeight = ref(450)
|
|
|
|
|
+const canvasRatio = ref('4:3')
|
|
|
|
|
+const zoomLevel = ref(100)
|
|
|
|
|
+
|
|
|
|
|
+const layers = ref([])
|
|
|
|
|
+const selectedLayerId = ref(null)
|
|
|
|
|
+const maintainAspectRatio = ref(true) // 默认启用等比例缩放
|
|
|
|
|
+
|
|
|
|
|
+// 拖拽状态
|
|
|
|
|
+const dragState = reactive({
|
|
|
|
|
+ draggingId: null,
|
|
|
|
|
+ dragOverId: null,
|
|
|
|
|
+ draggedLayer: null
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// 画布比例配置 - 使用更小的默认尺寸以适应屏幕
|
|
|
|
|
+const ratioConfig = {
|
|
|
|
|
+ '1:1': { width: 500, height: 500, ratio: 1 },
|
|
|
|
|
+ '4:3': { width: 600, height: 450, ratio: 4/3 },
|
|
|
|
|
+ '3:4': { width: 450, height: 600, ratio: 3/4 },
|
|
|
|
|
+ '16:9': { width: 640, height: 360, ratio: 16/9 },
|
|
|
|
|
+ '9:16': { width: 360, height: 640, ratio: 9/16 },
|
|
|
|
|
+ '3:2': { width: 600, height: 400, ratio: 3/2 },
|
|
|
|
|
+ '2:3': { width: 400, height: 600, ratio: 2/3 }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const selectedLayer = computed(() => {
|
|
|
|
|
+ return layers.value.find(l => l.id === selectedLayerId.value) || null
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const reversedLayers = computed(() => {
|
|
|
|
|
+ return [...layers.value].reverse()
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const selectedLayerIndex = computed(() => {
|
|
|
|
|
+ return layers.value.findIndex(l => l.id === selectedLayerId.value)
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const canMoveUp = computed(() => {
|
|
|
|
|
+ return selectedLayerId.value && selectedLayerIndex.value < layers.value.length - 1
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const canMoveDown = computed(() => {
|
|
|
|
|
+ return selectedLayerId.value && selectedLayerIndex.value > 0
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+let layerIdCounter = 0
|
|
|
|
|
+
|
|
|
|
|
+const beforeUpload = (file) => {
|
|
|
|
|
+ const isImage = file.type.startsWith('image/')
|
|
|
|
|
+ const isLt10M = file.size / 1024 / 1024 < 10
|
|
|
|
|
+
|
|
|
|
|
+ if (!isImage) {
|
|
|
|
|
+ ElMessage.error('只能上传图片文件!')
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!isLt10M) {
|
|
|
|
|
+ ElMessage.error('图片大小不能超过 10MB!')
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+ return true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handleUpload = (options) => {
|
|
|
|
|
+ const { file } = options
|
|
|
|
|
+ const reader = new FileReader()
|
|
|
|
|
+
|
|
|
|
|
+ reader.onload = (e) => {
|
|
|
|
|
+ const img = new Image()
|
|
|
|
|
+ img.onload = () => {
|
|
|
|
|
+ const maxSize = 300
|
|
|
|
|
+ let width = img.width
|
|
|
|
|
+ let height = img.height
|
|
|
|
|
+
|
|
|
|
|
+ if (width > maxSize || height > maxSize) {
|
|
|
|
|
+ const ratio = Math.min(maxSize / width, maxSize / height)
|
|
|
|
|
+ width = width * ratio
|
|
|
|
|
+ height = height * ratio
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const newLayer = {
|
|
|
|
|
+ id: ++layerIdCounter,
|
|
|
|
|
+ name: file.name,
|
|
|
|
|
+ url: e.target.result,
|
|
|
|
|
+ x: (canvasWidth.value - width) / 2,
|
|
|
|
|
+ y: (canvasHeight.value - height) / 2,
|
|
|
|
|
+ width: width,
|
|
|
|
|
+ height: height,
|
|
|
|
|
+ rotation: 0,
|
|
|
|
|
+ opacity: 100,
|
|
|
|
|
+ visible: true,
|
|
|
|
|
+ locked: false,
|
|
|
|
|
+ originalWidth: img.width,
|
|
|
|
|
+ originalHeight: img.height
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ layers.value.push(newLayer)
|
|
|
|
|
+ selectedLayerId.value = newLayer.id
|
|
|
|
|
+ ElMessage.success('素材上传成功!')
|
|
|
|
|
+ }
|
|
|
|
|
+ img.src = e.target.result
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ reader.readAsDataURL(file)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 添加文字图层
|
|
|
|
|
+const addTextLayer = () => {
|
|
|
|
|
+ const newLayer = {
|
|
|
|
|
+ id: ++layerIdCounter,
|
|
|
|
|
+ name: '文字 ' + (textLayerCount.value + 1),
|
|
|
|
|
+ type: 'text',
|
|
|
|
|
+ text: '双击编辑文字',
|
|
|
|
|
+ x: canvasWidth.value / 2 - 50,
|
|
|
|
|
+ y: canvasHeight.value / 2 - 15,
|
|
|
|
|
+ width: 100,
|
|
|
|
|
+ height: 30,
|
|
|
|
|
+ rotation: 0,
|
|
|
|
|
+ opacity: 100,
|
|
|
|
|
+ visible: true,
|
|
|
|
|
+ locked: false,
|
|
|
|
|
+ fontSize: 16,
|
|
|
|
|
+ fontFamily: 'Arial',
|
|
|
|
|
+ fontWeight: 'normal',
|
|
|
|
|
+ fontStyle: 'normal',
|
|
|
|
|
+ textDecoration: 'none',
|
|
|
|
|
+ color: '#000000',
|
|
|
|
|
+ textAlign: 'left',
|
|
|
|
|
+ backgroundColor: 'transparent',
|
|
|
|
|
+ lineHeight: 1.5,
|
|
|
|
|
+ letterSpacing: 0
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ layers.value.push(newLayer)
|
|
|
|
|
+ selectedLayerId.value = newLayer.id
|
|
|
|
|
+ textLayerCount.value++
|
|
|
|
|
+ ElMessage.success('文字图层已添加')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const textLayerCount = ref(0)
|
|
|
|
|
+
|
|
|
|
|
+// 素材库状态
|
|
|
|
|
+const materials = ref([])
|
|
|
|
|
+const materialsLoading = ref(false)
|
|
|
|
|
+
|
|
|
|
|
+// 获取素材库数据
|
|
|
|
|
+const fetchMaterials = async () => {
|
|
|
|
|
+ console.log('fetchMaterials called')
|
|
|
|
|
+ try {
|
|
|
|
|
+ materialsLoading.value = true
|
|
|
|
|
+ console.log('开始获取素材库数据')
|
|
|
|
|
+ const response = await Material_List()
|
|
|
|
|
+ console.log('素材库数据:', response)
|
|
|
|
|
+ if (response.code === 0) {
|
|
|
|
|
+ materials.value = response.data
|
|
|
|
|
+ console.log('素材库数据更新成功:', materials.value)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.error('获取素材库失败:', response)
|
|
|
|
|
+ ElMessage.error('获取素材库失败')
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('获取素材库错误:', error)
|
|
|
|
|
+ ElMessage.error('获取素材库失败')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ materialsLoading.value = false
|
|
|
|
|
+ console.log('获取素材库完成')
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 标签页点击事件
|
|
|
|
|
+const handleTabClick = (tab) => {
|
|
|
|
|
+ console.log('Tab clicked:', tab.props.name)
|
|
|
|
|
+ if (tab.props.name === 'material') {
|
|
|
|
|
+ fetchMaterials()
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 点击素材添加到画布
|
|
|
|
|
+const addMaterialToCanvas = (material) => {
|
|
|
|
|
+ const img = new Image()
|
|
|
|
|
+ img.onload = function() {
|
|
|
|
|
+ let width = img.width
|
|
|
|
|
+ let height = img.height
|
|
|
|
|
+
|
|
|
|
|
+ const maxSize = 200
|
|
|
|
|
+ if (width > maxSize || height > maxSize) {
|
|
|
|
|
+ const ratio = Math.min(maxSize / width, maxSize / height)
|
|
|
|
|
+ width = width * ratio
|
|
|
|
|
+ height = height * ratio
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const newLayer = {
|
|
|
|
|
+ id: ++layerIdCounter,
|
|
|
|
|
+ name: `素材 ${layerIdCounter}`,
|
|
|
|
|
+ url: material.material_url,
|
|
|
|
|
+ x: (canvasWidth.value - width) / 2,
|
|
|
|
|
+ y: (canvasHeight.value - height) / 2,
|
|
|
|
|
+ width: width,
|
|
|
|
|
+ height: height,
|
|
|
|
|
+ rotation: 0,
|
|
|
|
|
+ opacity: 100,
|
|
|
|
|
+ visible: true,
|
|
|
|
|
+ locked: false,
|
|
|
|
|
+ originalWidth: img.width,
|
|
|
|
|
+ originalHeight: img.height
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ layers.value.push(newLayer)
|
|
|
|
|
+ selectedLayerId.value = newLayer.id
|
|
|
|
|
+ ElMessage.success('素材已添加到画布!')
|
|
|
|
|
+ }
|
|
|
|
|
+ img.src = material.material_url
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 组件挂载时获取素材库数据
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ fetchMaterials()
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const getLayerStyle = (layer) => {
|
|
|
|
|
+ if (!layer.visible) {
|
|
|
|
|
+ return { display: 'none' }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ left: layer.x + 'px',
|
|
|
|
|
+ top: layer.y + 'px',
|
|
|
|
|
+ width: layer.type === 'text' ? 'auto' : layer.width + 'px',
|
|
|
|
|
+ height: layer.type === 'text' ? 'auto' : layer.height + 'px',
|
|
|
|
|
+ transform: `rotate(${layer.rotation}deg)`,
|
|
|
|
|
+ opacity: layer.opacity / 100
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 获取文字图层样式
|
|
|
|
|
+const getTextStyle = (layer) => {
|
|
|
|
|
+ return {
|
|
|
|
|
+ fontSize: layer.fontSize + 'px',
|
|
|
|
|
+ fontFamily: layer.fontFamily,
|
|
|
|
|
+ fontWeight: layer.fontWeight,
|
|
|
|
|
+ fontStyle: layer.fontStyle,
|
|
|
|
|
+ textDecoration: layer.textDecoration,
|
|
|
|
|
+ color: layer.color,
|
|
|
|
|
+ textAlign: layer.textAlign,
|
|
|
|
|
+ backgroundColor: layer.backgroundColor,
|
|
|
|
|
+ lineHeight: layer.lineHeight,
|
|
|
|
|
+ letterSpacing: layer.letterSpacing + 'px',
|
|
|
|
|
+ padding: '4px 8px',
|
|
|
|
|
+ whiteSpace: 'nowrap',
|
|
|
|
|
+ userSelect: 'none'
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 双击编辑文字
|
|
|
|
|
+const editingTextLayer = ref(null)
|
|
|
|
|
+
|
|
|
|
|
+const handleLayerDblClick = (e, layer) => {
|
|
|
|
|
+ if (layer.type === 'text' && !layer.locked) {
|
|
|
|
|
+ editingTextLayer.value = layer
|
|
|
|
|
+ // 创建输入框进行编辑
|
|
|
|
|
+ const input = document.createElement('input')
|
|
|
|
|
+ input.value = layer.text
|
|
|
|
|
+ input.style.cssText = `
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ left: ${layer.x}px;
|
|
|
|
|
+ top: ${layer.y}px;
|
|
|
|
|
+ font-size: ${layer.fontSize}px;
|
|
|
|
|
+ font-family: ${layer.fontFamily};
|
|
|
|
|
+ font-weight: ${layer.fontWeight};
|
|
|
|
|
+ font-style: ${layer.fontStyle};
|
|
|
|
|
+ color: ${layer.color};
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border: 2px solid #409eff;
|
|
|
|
|
+ padding: 4px 8px;
|
|
|
|
|
+ z-index: 1000;
|
|
|
|
|
+ outline: none;
|
|
|
|
|
+ `
|
|
|
|
|
+
|
|
|
|
|
+ const canvas = canvasRef.value
|
|
|
|
|
+ canvas.appendChild(input)
|
|
|
|
|
+ input.focus()
|
|
|
|
|
+ input.select()
|
|
|
|
|
+
|
|
|
|
|
+ const saveEdit = () => {
|
|
|
|
|
+ layer.text = input.value || '双击编辑文字'
|
|
|
|
|
+ canvas.removeChild(input)
|
|
|
|
|
+ editingTextLayer.value = null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ input.addEventListener('blur', saveEdit)
|
|
|
|
|
+ input.addEventListener('keydown', (e) => {
|
|
|
|
|
+ if (e.key === 'Enter') {
|
|
|
|
|
+ saveEdit()
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 画布比例变化处理
|
|
|
|
|
+const handleCanvasRatioChange = (ratio) => {
|
|
|
|
|
+ if (ratio === 'custom') return
|
|
|
|
|
+
|
|
|
|
|
+ const config = ratioConfig[ratio]
|
|
|
|
|
+ if (config) {
|
|
|
|
|
+ canvasWidth.value = config.width
|
|
|
|
|
+ canvasHeight.value = config.height
|
|
|
|
|
+ ElMessage.success(`画布尺寸已设置为 ${ratio}`)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 画布尺寸变化处理
|
|
|
|
|
+const handleCanvasSizeChange = () => {
|
|
|
|
|
+ // 当手动调整尺寸时,重置比例为自定义
|
|
|
|
|
+ if (canvasRatio.value !== 'custom') {
|
|
|
|
|
+ const config = ratioConfig[canvasRatio.value]
|
|
|
|
|
+ if (config) {
|
|
|
|
|
+ const currentRatio = canvasWidth.value / canvasHeight.value
|
|
|
|
|
+ const expectedRatio = config.ratio
|
|
|
|
|
+ // 如果比例偏差较大,切换到自定义
|
|
|
|
|
+ if (Math.abs(currentRatio - expectedRatio) > 0.1) {
|
|
|
|
|
+ canvasRatio.value = 'custom'
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 图片宽度变化时,如果锁定比例,自动调整高度
|
|
|
|
|
+const handleWidthChange = (newWidth) => {
|
|
|
|
|
+ if (!maintainAspectRatio.value || !selectedLayer.value) return
|
|
|
|
|
+
|
|
|
|
|
+ const layer = selectedLayer.value
|
|
|
|
|
+ const aspectRatio = layer.originalWidth / layer.originalHeight
|
|
|
|
|
+ layer.height = Math.round(newWidth / aspectRatio)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 图片高度变化时,如果锁定比例,自动调整宽度
|
|
|
|
|
+const handleHeightChange = (newHeight) => {
|
|
|
|
|
+ if (!maintainAspectRatio.value || !selectedLayer.value) return
|
|
|
|
|
+
|
|
|
|
|
+ const layer = selectedLayer.value
|
|
|
|
|
+ const aspectRatio = layer.originalWidth / layer.originalHeight
|
|
|
|
|
+ layer.width = Math.round(newHeight * aspectRatio)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const selectLayer = (id) => {
|
|
|
|
|
+ selectedLayerId.value = id
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const toggleLayerVisibility = (id) => {
|
|
|
|
|
+ const layer = layers.value.find(l => l.id === id)
|
|
|
|
|
+ if (layer) {
|
|
|
|
|
+ layer.visible = !layer.visible
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const toggleLayerLock = (id) => {
|
|
|
|
|
+ const layer = layers.value.find(l => l.id === id)
|
|
|
|
|
+ if (layer) {
|
|
|
|
|
+ layer.locked = !layer.locked
|
|
|
|
|
+ ElMessage.success(layer.locked ? '图层已锁定' : '图层已解锁')
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const moveLayerUp = () => {
|
|
|
|
|
+ const index = selectedLayerIndex.value
|
|
|
|
|
+ if (index < layers.value.length - 1) {
|
|
|
|
|
+ const temp = layers.value[index]
|
|
|
|
|
+ layers.value[index] = layers.value[index + 1]
|
|
|
|
|
+ layers.value[index + 1] = temp
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const moveLayerDown = () => {
|
|
|
|
|
+ const index = selectedLayerIndex.value
|
|
|
|
|
+ if (index > 0) {
|
|
|
|
|
+ const temp = layers.value[index]
|
|
|
|
|
+ layers.value[index] = layers.value[index - 1]
|
|
|
|
|
+ layers.value[index - 1] = temp
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 拖拽排序相关函数
|
|
|
|
|
+const handleDragStart = (e, layer) => {
|
|
|
|
|
+ // 如果图层被锁定,禁止拖拽排序
|
|
|
|
|
+ if (layer.locked) {
|
|
|
|
|
+ e.preventDefault()
|
|
|
|
|
+ ElMessage.warning('图层已锁定,无法排序')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ dragState.draggingId = layer.id
|
|
|
|
|
+ dragState.draggedLayer = layer
|
|
|
|
|
+ e.dataTransfer.effectAllowed = 'move'
|
|
|
|
|
+ e.dataTransfer.setData('text/plain', layer.id)
|
|
|
|
|
+ // 设置拖拽时的半透明效果
|
|
|
|
|
+ e.target.style.opacity = '0.5'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handleDragOver = (e, layer) => {
|
|
|
|
|
+ e.preventDefault()
|
|
|
|
|
+ e.dataTransfer.dropEffect = 'move'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handleDragEnter = (e, layer) => {
|
|
|
|
|
+ e.preventDefault()
|
|
|
|
|
+ if (dragState.draggingId !== layer.id) {
|
|
|
|
|
+ dragState.dragOverId = layer.id
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handleDragLeave = (e) => {
|
|
|
|
|
+ // 只有当离开当前元素时才清除dragOverId
|
|
|
|
|
+ if (!e.currentTarget.contains(e.relatedTarget)) {
|
|
|
|
|
+ dragState.dragOverId = null
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handleDrop = (e, targetLayer) => {
|
|
|
|
|
+ e.preventDefault()
|
|
|
|
|
+ e.stopPropagation()
|
|
|
|
|
+
|
|
|
|
|
+ const draggedId = dragState.draggingId
|
|
|
|
|
+ const targetId = targetLayer.id
|
|
|
|
|
+
|
|
|
|
|
+ if (draggedId && draggedId !== targetId) {
|
|
|
|
|
+ // 获取原始索引(在layers数组中的索引,不是reversedLayers)
|
|
|
|
|
+ const draggedIndex = layers.value.findIndex(l => l.id === draggedId)
|
|
|
|
|
+ const targetIndex = layers.value.findIndex(l => l.id === targetId)
|
|
|
|
|
+
|
|
|
|
|
+ if (draggedIndex !== -1 && targetIndex !== -1) {
|
|
|
|
|
+ // 移动图层
|
|
|
|
|
+ const [movedLayer] = layers.value.splice(draggedIndex, 1)
|
|
|
|
|
+ layers.value.splice(targetIndex, 0, movedLayer)
|
|
|
|
|
+
|
|
|
|
|
+ ElMessage.success('图层排序已更新')
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 重置拖拽状态
|
|
|
|
|
+ dragState.dragOverId = null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handleDragEnd = (e) => {
|
|
|
|
|
+ // 恢复透明度
|
|
|
|
|
+ if (e.target) {
|
|
|
|
|
+ e.target.style.opacity = '1'
|
|
|
|
|
+ }
|
|
|
|
|
+ // 重置所有拖拽状态
|
|
|
|
|
+ dragState.draggingId = null
|
|
|
|
|
+ dragState.dragOverId = null
|
|
|
|
|
+ dragState.draggedLayer = null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const deleteLayer = () => {
|
|
|
|
|
+ const index = selectedLayerIndex.value
|
|
|
|
|
+ if (index !== -1) {
|
|
|
|
|
+ layers.value.splice(index, 1)
|
|
|
|
|
+ selectedLayerId.value = layers.value.length > 0 ? layers.value[0].id : null
|
|
|
|
|
+ ElMessage.success('图层已删除')
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+let isDragging = false
|
|
|
|
|
+let isResizing = false
|
|
|
|
|
+let isRotating = false
|
|
|
|
|
+let dragStartX = 0
|
|
|
|
|
+let dragStartY = 0
|
|
|
|
|
+let layerStartX = 0
|
|
|
|
|
+let layerStartY = 0
|
|
|
|
|
+let resizeDirection = ''
|
|
|
|
|
+let startWidth = 0
|
|
|
|
|
+let startHeight = 0
|
|
|
|
|
+let startRotation = 0
|
|
|
|
|
+let centerX = 0
|
|
|
|
|
+let centerY = 0
|
|
|
|
|
+
|
|
|
|
|
+const handleLayerMouseDown = (e, layer) => {
|
|
|
|
|
+ if (currentTool.value !== 'select' && currentTool.value !== 'move') return
|
|
|
|
|
+
|
|
|
|
|
+ // 如果图层被锁定,禁止拖拽
|
|
|
|
|
+ if (layer.locked) {
|
|
|
|
|
+ selectedLayerId.value = layer.id
|
|
|
|
|
+ ElMessage.warning('图层已锁定,无法移动')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ selectedLayerId.value = layer.id
|
|
|
|
|
+
|
|
|
|
|
+ isDragging = true
|
|
|
|
|
+ dragStartX = e.clientX
|
|
|
|
|
+ dragStartY = e.clientY
|
|
|
|
|
+ layerStartX = layer.x
|
|
|
|
|
+ layerStartY = layer.y
|
|
|
|
|
+
|
|
|
|
|
+ e.preventDefault()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handleCanvasMouseDown = (e) => {
|
|
|
|
|
+ if (e.target === canvasRef.value) {
|
|
|
|
|
+ selectedLayerId.value = null
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handleCanvasMouseMove = (e) => {
|
|
|
|
|
+ if (!selectedLayer.value) return
|
|
|
|
|
+
|
|
|
|
|
+ if (isDragging) {
|
|
|
|
|
+ const deltaX = e.clientX - dragStartX
|
|
|
|
|
+ const deltaY = e.clientY - dragStartY
|
|
|
|
|
+
|
|
|
|
|
+ selectedLayer.value.x = Math.round(layerStartX + deltaX)
|
|
|
|
|
+ selectedLayer.value.y = Math.round(layerStartY + deltaY)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (isResizing) {
|
|
|
|
|
+ const deltaX = e.clientX - dragStartX
|
|
|
|
|
+ const deltaY = e.clientY - dragStartY
|
|
|
|
|
+
|
|
|
|
|
+ const layer = selectedLayer.value
|
|
|
|
|
+ const aspectRatio = layer.originalWidth / layer.originalHeight
|
|
|
|
|
+
|
|
|
|
|
+ let newWidth = startWidth
|
|
|
|
|
+ let newHeight = startHeight
|
|
|
|
|
+
|
|
|
|
|
+ // 计算新的宽度和高度
|
|
|
|
|
+ if (resizeDirection.includes('e')) {
|
|
|
|
|
+ newWidth = Math.max(20, startWidth + deltaX)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (resizeDirection.includes('w')) {
|
|
|
|
|
+ newWidth = Math.max(20, startWidth - deltaX)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (resizeDirection.includes('s')) {
|
|
|
|
|
+ newHeight = Math.max(20, startHeight + deltaY)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (resizeDirection.includes('n')) {
|
|
|
|
|
+ newHeight = Math.max(20, startHeight - deltaY)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果锁定等比例,根据宽高比调整
|
|
|
|
|
+ if (maintainAspectRatio.value) {
|
|
|
|
|
+ if (resizeDirection === 'se' || resizeDirection === 'nw') {
|
|
|
|
|
+ // 对角线调整,以宽度为准
|
|
|
|
|
+ newHeight = newWidth / aspectRatio
|
|
|
|
|
+ } else if (resizeDirection === 'ne' || resizeDirection === 'sw') {
|
|
|
|
|
+ // 对角线调整,以宽度为准
|
|
|
|
|
+ newHeight = newWidth / aspectRatio
|
|
|
|
|
+ } else if (resizeDirection.includes('e') || resizeDirection.includes('w')) {
|
|
|
|
|
+ // 水平调整
|
|
|
|
|
+ newHeight = newWidth / aspectRatio
|
|
|
|
|
+ } else if (resizeDirection.includes('n') || resizeDirection.includes('s')) {
|
|
|
|
|
+ // 垂直调整
|
|
|
|
|
+ newWidth = newHeight * aspectRatio
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 应用新的尺寸和位置
|
|
|
|
|
+ if (resizeDirection.includes('w')) {
|
|
|
|
|
+ layer.x = Math.round(layerStartX + (startWidth - newWidth))
|
|
|
|
|
+ }
|
|
|
|
|
+ if (resizeDirection.includes('n')) {
|
|
|
|
|
+ layer.y = Math.round(layerStartY + (startHeight - newHeight))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ layer.width = Math.round(newWidth)
|
|
|
|
|
+ layer.height = Math.round(newHeight)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (isRotating) {
|
|
|
|
|
+ const rect = canvasRef.value.getBoundingClientRect()
|
|
|
|
|
+ const mouseX = e.clientX - rect.left
|
|
|
|
|
+ const mouseY = e.clientY - rect.top
|
|
|
|
|
+
|
|
|
|
|
+ const angle = Math.atan2(mouseY - centerY, mouseX - centerX) * 180 / Math.PI
|
|
|
|
|
+ selectedLayer.value.rotation = Math.round(angle + 90)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handleCanvasMouseUp = () => {
|
|
|
|
|
+ isDragging = false
|
|
|
|
|
+ isResizing = false
|
|
|
|
|
+ isRotating = false
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const startResize = (e, direction) => {
|
|
|
|
|
+ if (!selectedLayer.value) return
|
|
|
|
|
+
|
|
|
|
|
+ isResizing = true
|
|
|
|
|
+ resizeDirection = direction
|
|
|
|
|
+ dragStartX = e.clientX
|
|
|
|
|
+ dragStartY = e.clientY
|
|
|
|
|
+ startWidth = selectedLayer.value.width
|
|
|
|
|
+ startHeight = selectedLayer.value.height
|
|
|
|
|
+ layerStartX = selectedLayer.value.x
|
|
|
|
|
+ layerStartY = selectedLayer.value.y
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const startRotate = (e) => {
|
|
|
|
|
+ if (!selectedLayer.value) return
|
|
|
|
|
+
|
|
|
|
|
+ isRotating = true
|
|
|
|
|
+
|
|
|
|
|
+ const rect = canvasRef.value.getBoundingClientRect()
|
|
|
|
|
+ centerX = selectedLayer.value.x + selectedLayer.value.width / 2
|
|
|
|
|
+ centerY = selectedLayer.value.y + selectedLayer.value.height / 2
|
|
|
|
|
+ startRotation = selectedLayer.value.rotation
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handleCanvasWheel = (e) => {
|
|
|
|
|
+ e.preventDefault()
|
|
|
|
|
+ const delta = e.deltaY > 0 ? -10 : 10
|
|
|
|
|
+ zoomLevel.value = Math.max(10, Math.min(200, zoomLevel.value + delta))
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+.template-design-container {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ height: calc(100vh - 50px);
|
|
|
|
|
+ background-color: #f5f5f5;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.toolbar {
|
|
|
|
|
+ width: 220px;
|
|
|
|
|
+ background-color: #fff;
|
|
|
|
|
+ border-right: 1px solid #ddd;
|
|
|
|
|
+ padding: 8px 12px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.toolbar h3 {
|
|
|
|
|
+ margin: 0 0 8px 0;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.toolbar-tabs {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.toolbar-tabs :deep(.el-tabs__header) {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.toolbar-tabs :deep(.el-tabs__content) {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.toolbar-tabs :deep(.el-tab-pane) {
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.toolbar-tabs :deep(.material-tab) {
|
|
|
|
|
+ color: red !important;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 完整素材库样式 */
|
|
|
|
|
+.materials-library-full {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.materials-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.materials-header h4 {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.materials-list-full {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ padding: 4px;
|
|
|
|
|
+ background-color: #f9f9f9;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.material-item-full {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ aspect-ratio: 1;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ border: 2px solid #ddd;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.material-item-full:hover {
|
|
|
|
|
+ border-color: #409eff;
|
|
|
|
|
+ transform: scale(1.03);
|
|
|
|
|
+ z-index: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.material-item-full img {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ object-fit: cover;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.material-item-full .material-overlay {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ background: rgba(0, 0, 0, 0.6);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ opacity: 0;
|
|
|
|
|
+ transition: opacity 0.2s;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.material-item-full:hover .material-overlay {
|
|
|
|
|
+ opacity: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.material-item-full .material-overlay .el-icon {
|
|
|
|
|
+ font-size: 24px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tool-buttons {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.canvas-settings {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.canvas-settings h4,
|
|
|
|
|
+.layer-info h4 {
|
|
|
|
|
+ margin: 0 0 6px 0;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.layer-info {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+ padding-right: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.property-item {
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.property-item span {
|
|
|
|
|
+ width: 40px;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.property-item .el-input-number {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.property-item .el-slider {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 缩小表单元素 */
|
|
|
|
|
+:deep(.el-input-number--small) {
|
|
|
|
|
+ width: 100% !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.el-input-number--small .el-input__wrapper) {
|
|
|
|
|
+ padding: 0 4px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.el-button--small) {
|
|
|
|
|
+ padding: 4px 8px;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.el-select--small) {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.el-switch__label) {
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 修复滑块样式 */
|
|
|
|
|
+:deep(.el-slider) {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.el-slider__runway) {
|
|
|
|
|
+ margin: 8px 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.el-slider__button-wrapper) {
|
|
|
|
|
+ z-index: 10;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.canvas-area {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ background-color: #e8e8e8;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.canvas-wrapper {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ padding: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.canvas {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ background-color: #fff;
|
|
|
|
|
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.layer {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ cursor: move;
|
|
|
|
|
+ user-select: none;
|
|
|
|
|
+ border: 2px solid transparent;
|
|
|
|
|
+ transition: border-color 0.2s;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.layer.selected {
|
|
|
|
|
+ border-color: #409eff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.layer img {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ pointer-events: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.text-content {
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ min-width: 50px;
|
|
|
|
|
+ cursor: text;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.text-layer {
|
|
|
|
|
+ border: 1px dashed #ccc;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.text-layer.selected {
|
|
|
|
|
+ border-color: #409eff;
|
|
|
|
|
+ border-style: solid;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.resize-handle {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ width: 10px;
|
|
|
|
|
+ height: 10px;
|
|
|
|
|
+ background-color: #409eff;
|
|
|
|
|
+ border: 2px solid #fff;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ z-index: 10;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.resize-handle.nw {
|
|
|
|
|
+ top: -5px;
|
|
|
|
|
+ left: -5px;
|
|
|
|
|
+ cursor: nw-resize;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.resize-handle.ne {
|
|
|
|
|
+ top: -5px;
|
|
|
|
|
+ right: -5px;
|
|
|
|
|
+ cursor: ne-resize;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.resize-handle.sw {
|
|
|
|
|
+ bottom: -5px;
|
|
|
|
|
+ left: -5px;
|
|
|
|
|
+ cursor: sw-resize;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.resize-handle.se {
|
|
|
|
|
+ bottom: -5px;
|
|
|
|
|
+ right: -5px;
|
|
|
|
|
+ cursor: se-resize;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.rotate-handle {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: -30px;
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ transform: translateX(-50%);
|
|
|
|
|
+ width: 16px;
|
|
|
|
|
+ height: 16px;
|
|
|
|
|
+ background-color: #409eff;
|
|
|
|
|
+ border: 2px solid #fff;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ cursor: grab;
|
|
|
|
|
+ z-index: 10;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.rotate-handle::before {
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 100%;
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ transform: translateX(-50%);
|
|
|
|
|
+ width: 1px;
|
|
|
|
|
+ height: 14px;
|
|
|
|
|
+ background-color: #409eff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.layer-panel {
|
|
|
|
|
+ width: 200px;
|
|
|
|
|
+ background-color: #fff;
|
|
|
|
|
+ border-left: 1px solid #ddd;
|
|
|
|
|
+ padding: 8px 12px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.layer-panel h3 {
|
|
|
|
|
+ margin: 0 0 8px 0;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.layer-actions {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.layer-list {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ margin-top: 6px;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.layer-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 4px 6px;
|
|
|
|
|
+ margin-bottom: 3px;
|
|
|
|
|
+ background-color: #f9f9f9;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ user-select: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.layer-item:hover {
|
|
|
|
|
+ background-color: #f0f0f0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.layer-item.selected {
|
|
|
|
|
+ background-color: #e6f7ff;
|
|
|
|
|
+ border: 1px solid #91d5ff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.layer-item.dragging {
|
|
|
|
|
+ opacity: 0.5;
|
|
|
|
|
+ cursor: grabbing;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.layer-item.drag-over {
|
|
|
|
|
+ background-color: #d9ecff;
|
|
|
|
|
+ border: 2px dashed #409eff;
|
|
|
|
|
+ transform: scale(1.02);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.drag-handle {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+ cursor: grab;
|
|
|
|
|
+ transition: color 0.2s;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.drag-handle:hover {
|
|
|
|
|
+ color: #409eff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.layer-item.dragging .drag-handle {
|
|
|
|
|
+ cursor: grabbing;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.visibility-icon {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.visibility-icon:hover {
|
|
|
|
|
+ color: #409eff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.lock-icon {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: color 0.2s;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.lock-icon:hover {
|
|
|
|
|
+ color: #f56c6c;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.layer-item.locked {
|
|
|
|
|
+ opacity: 0.7;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.layer-item.locked .drag-handle {
|
|
|
|
|
+ cursor: not-allowed;
|
|
|
|
|
+ color: #ccc;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.layer-thumbnail {
|
|
|
|
|
+ width: 32px;
|
|
|
|
|
+ height: 32px;
|
|
|
|
|
+ object-fit: cover;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ border: 1px solid #ddd;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.text-thumbnail {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ background-color: #f0f0f0;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.layer-name {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.empty-tip {
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+ padding: 10px;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.el-divider {
|
|
|
|
|
+ margin: 6px 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 隐藏滚动条但保持功能 */
|
|
|
|
|
+.layer-list::-webkit-scrollbar,
|
|
|
|
|
+.layer-info::-webkit-scrollbar {
|
|
|
|
|
+ width: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.layer-list::-webkit-scrollbar-thumb,
|
|
|
|
|
+.layer-info::-webkit-scrollbar-thumb {
|
|
|
|
|
+ background-color: #ddd;
|
|
|
|
|
+ border-radius: 2px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.layer-list::-webkit-scrollbar-track,
|
|
|
|
|
+.layer-info::-webkit-scrollbar-track {
|
|
|
|
|
+ background-color: transparent;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 覆盖 admin-box 的 margin-bottom */
|
|
|
|
|
+:deep(.admin-box) {
|
|
|
|
|
+ margin-bottom: 0 !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 确保容器占满整个视口 */
|
|
|
|
|
+.template-design-container {
|
|
|
|
|
+ margin: 0 !important;
|
|
|
|
|
+ padding: 0 !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 素材库样式 */
|
|
|
|
|
+.materials-library {
|
|
|
|
|
+ margin: 8px 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.materials-library h4 {
|
|
|
|
|
+ margin: 0 0 8px 0;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.materials-list {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ max-height: 200px;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ padding: 4px;
|
|
|
|
|
+ background-color: #f9f9f9;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.material-item {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ aspect-ratio: 1;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ border: 1px solid #ddd;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.material-item:hover {
|
|
|
|
|
+ border-color: #409eff;
|
|
|
|
|
+ transform: scale(1.05);
|
|
|
|
|
+ z-index: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.material-item img {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ object-fit: cover;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.material-overlay {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ background: rgba(0, 0, 0, 0.5);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ opacity: 0;
|
|
|
|
|
+ transition: opacity 0.2s;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ font-size: 20px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.material-item:hover .material-overlay {
|
|
|
|
|
+ opacity: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.empty-materials {
|
|
|
|
|
+ grid-column: 1 / -1;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.materials-list::-webkit-scrollbar {
|
|
|
|
|
+ width: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.materials-list::-webkit-scrollbar-thumb {
|
|
|
|
|
+ background-color: #ddd;
|
|
|
|
|
+ border-radius: 2px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.materials-list::-webkit-scrollbar-track {
|
|
|
|
|
+ background-color: transparent;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|