TemplateDesign.vue 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696
  1. <template>
  2. <div class="template-design-container">
  3. <!-- 左侧工具栏 -->
  4. <div class="toolbar">
  5. <!-- 标签页切换 -->
  6. <div class="toolbar-tabs">
  7. <el-tabs v-model="activeTab" type="card" @tab-click="handleTabClick">
  8. <el-tab-pane label="模版设计" name="design">
  9. <el-upload
  10. class="custom-upload"
  11. drag
  12. :show-file-list="false"
  13. :before-upload="beforeUpload"
  14. :http-request="handleUpload"
  15. accept="image/jpeg,image/png,image/jpg,image/webp"
  16. multiple
  17. >
  18. <div class="upload-main">
  19. <div class="upload-inner-button">
  20. <el-icon class="custom-upload-icon">
  21. <Upload />
  22. </el-icon>
  23. <span class="upload-inner-text">上传素材图</span>
  24. </div>
  25. </div>
  26. <div class="el-upload__tip">
  27. 拖拽或点击上传JPEG/JPG/PNG 10M以内
  28. </div>
  29. </el-upload>
  30. <el-button type="success" :icon="Plus" style="width: 100%;" @click="addTextLayer">
  31. 添加文字
  32. </el-button>
  33. <el-divider />
  34. <!-- 画布尺寸设置 -->
  35. <div class="canvas-settings">
  36. <h4>画布尺寸</h4>
  37. <div class="property-item">
  38. <span>比例:</span>
  39. <el-select v-model="canvasRatio" size="small" style="flex: 1;" @change="handleCanvasRatioChange">
  40. <el-option label="自定义" value="custom" />
  41. <el-option label="1:1 (正方形)" value="1:1" />
  42. <el-option label="4:3 (横屏)" value="4:3" />
  43. <el-option label="3:4 (竖屏)" value="3:4" />
  44. <el-option label="16:9 (宽屏)" value="16:9" />
  45. <el-option label="9:16 (手机)" value="9:16" />
  46. <el-option label="3:2 (照片)" value="3:2" />
  47. <el-option label="2:3 (竖照片)" value="2:3" />
  48. </el-select>
  49. </div>
  50. <div class="property-item">
  51. <span>宽度:</span>
  52. <el-input-number v-model="canvasWidth" size="small" :step="10" :min="100" :max="2000" @change="handleCanvasSizeChange" />
  53. </div>
  54. <div class="property-item">
  55. <span>高度:</span>
  56. <el-input-number v-model="canvasHeight" size="small" :step="10" :min="100" :max="2000" @change="handleCanvasSizeChange" />
  57. </div>
  58. </div>
  59. <el-divider />
  60. <div class="layer-info" v-if="selectedLayer">
  61. <h4>图层属性</h4>
  62. <div class="property-item">
  63. <span>X:</span>
  64. <el-input-number v-model="selectedLayer.x" size="small" :step="1" />
  65. </div>
  66. <div class="property-item">
  67. <span>Y:</span>
  68. <el-input-number v-model="selectedLayer.y" size="small" :step="1" />
  69. </div>
  70. <div class="property-item">
  71. <span>宽度:</span>
  72. <el-input-number
  73. v-model="selectedLayer.width"
  74. size="small"
  75. :step="1"
  76. @change="handleWidthChange"
  77. />
  78. </div>
  79. <div class="property-item">
  80. <span>高度:</span>
  81. <el-input-number
  82. v-model="selectedLayer.height"
  83. size="small"
  84. :step="1"
  85. @change="handleHeightChange"
  86. />
  87. </div>
  88. <div class="property-item">
  89. <span>旋转:</span>
  90. <el-input-number v-model="selectedLayer.rotation" size="small" :step="1" :min="-360" :max="360" />
  91. </div>
  92. <div class="property-item">
  93. <span>透明度:</span>
  94. <el-slider v-model="selectedLayer.opacity" :min="0" :max="100" />
  95. </div>
  96. <!-- 文字图层特有属性 -->
  97. <template v-if="selectedLayer.type === 'text'">
  98. <el-divider />
  99. <h4>文字属性</h4>
  100. <div class="property-item">
  101. <span>内容:</span>
  102. <el-input
  103. v-model="selectedLayer.text"
  104. size="small"
  105. style="flex: 1;"
  106. />
  107. </div>
  108. <div class="property-item">
  109. <span>字体:</span>
  110. <el-select v-model="selectedLayer.fontFamily" size="small" style="flex: 1;">
  111. <el-option label="Arial" value="Arial" />
  112. <el-option label="宋体" value="SimSun" />
  113. <el-option label="黑体" value="SimHei" />
  114. <el-option label="微软雅黑" value="Microsoft YaHei" />
  115. <el-option label="楷体" value="KaiTi" />
  116. <el-option label="Times New Roman" value="Times New Roman" />
  117. </el-select>
  118. </div>
  119. <div class="property-item">
  120. <span>字号:</span>
  121. <el-input-number v-model="selectedLayer.fontSize" size="small" :min="8" :max="200" :step="1" />
  122. </div>
  123. <div class="property-item">
  124. <span>颜色:</span>
  125. <el-color-picker v-model="selectedLayer.color" size="small" />
  126. </div>
  127. <div class="property-item">
  128. <span>背景:</span>
  129. <el-color-picker v-model="selectedLayer.backgroundColor" size="small" show-alpha />
  130. </div>
  131. <div class="property-item">
  132. <span>对齐:</span>
  133. <el-radio-group v-model="selectedLayer.textAlign" size="small">
  134. <el-radio-button label="left">左</el-radio-button>
  135. <el-radio-button label="center">中</el-radio-button>
  136. <el-radio-button label="right">右</el-radio-button>
  137. </el-radio-group>
  138. </div>
  139. <div class="property-item">
  140. <span>加粗:</span>
  141. <el-switch v-model="selectedLayer.fontWeight" active-value="bold" inactive-value="normal" />
  142. </div>
  143. <div class="property-item">
  144. <span>斜体:</span>
  145. <el-switch v-model="selectedLayer.fontStyle" active-value="italic" inactive-value="normal" />
  146. </div>
  147. <div class="property-item">
  148. <span>下划线:</span>
  149. <el-switch v-model="selectedLayer.textDecoration" active-value="underline" inactive-value="none" />
  150. </div>
  151. <div class="property-item">
  152. <span>行高:</span>
  153. <el-slider v-model="selectedLayer.lineHeight" :min="0.5" :max="3" :step="0.1" />
  154. </div>
  155. <div class="property-item">
  156. <span>字距:</span>
  157. <el-slider v-model="selectedLayer.letterSpacing" :min="-5" :max="20" :step="1" />
  158. </div>
  159. </template>
  160. </div>
  161. <!-- 保存模版按钮 -->
  162. <el-divider />
  163. </el-tab-pane>
  164. <el-tab-pane label="素材选择" name="material" :label-class="'material-tab'">
  165. <div class="materials-list-full">
  166. <el-skeleton v-if="materialsLoading" :rows="5" animated />
  167. <div v-else-if="materials.length === 0" class="empty-materials">
  168. 暂无素材
  169. </div>
  170. <div
  171. v-else
  172. class="material-item-full"
  173. v-for="material in materials"
  174. :key="material.id"
  175. @click="addMaterialToCanvas(material)"
  176. >
  177. <img :src="material.material_url" :alt="material.id" />
  178. <div class="material-overlay">
  179. <el-icon><Plus /></el-icon>
  180. <span>添加</span>
  181. </div>
  182. </div>
  183. </div>
  184. </el-tab-pane>
  185. </el-tabs>
  186. </div>
  187. <!-- 固定在底部的保存模版按钮 -->
  188. <el-divider />
  189. <el-button
  190. type="primary"
  191. :icon="Document"
  192. class="save-template-btn"
  193. @click="saveTemplate"
  194. >
  195. 保存模版
  196. </el-button>
  197. </div>
  198. <!-- 中间画布区域 -->
  199. <div class="canvas-area" ref="canvasAreaRef">
  200. <div class="canvas-wrapper">
  201. <div
  202. ref="canvasRef"
  203. class="canvas"
  204. :style="{
  205. width: canvasWidth + 'px',
  206. height: canvasHeight + 'px'
  207. }"
  208. @mousedown="handleCanvasMouseDown"
  209. @mousemove="handleCanvasMouseMove"
  210. @mouseup="handleCanvasMouseUp"
  211. @mouseleave="handleCanvasMouseUp"
  212. @wheel="handleCanvasWheel"
  213. >
  214. <div
  215. v-for="(layer, index) in layers"
  216. :key="layer.id"
  217. class="layer"
  218. :class="{
  219. 'selected': selectedLayerId === layer.id,
  220. 'text-layer': layer.type === 'text'
  221. }"
  222. :style="getLayerStyle(layer)"
  223. @mousedown.stop="handleLayerMouseDown($event, layer)"
  224. @dblclick.stop="handleLayerDblClick($event, layer)"
  225. >
  226. <!-- 图片图层 -->
  227. <template v-if="layer.type !== 'text'">
  228. <img :src="layer.url" :alt="layer.name" draggable="false" />
  229. </template>
  230. <!-- 文字图层 -->
  231. <template v-else>
  232. <div
  233. class="text-content"
  234. :style="getTextStyle(layer)"
  235. contenteditable="false"
  236. >
  237. {{ layer.text }}
  238. </div>
  239. </template>
  240. <!-- 选中状态的调整手柄 -->
  241. <template v-if="selectedLayerId === layer.id">
  242. <div class="resize-handle nw" @mousedown.stop="startResize($event, 'nw')"></div>
  243. <div class="resize-handle ne" @mousedown.stop="startResize($event, 'ne')"></div>
  244. <div class="resize-handle sw" @mousedown.stop="startResize($event, 'sw')"></div>
  245. <div class="resize-handle se" @mousedown.stop="startResize($event, 'se')"></div>
  246. <div class="rotate-handle" @mousedown.stop="startRotate($event)"></div>
  247. </template>
  248. </div>
  249. </div>
  250. </div>
  251. </div>
  252. <!-- 右侧图层面板 -->
  253. <div class="layer-panel">
  254. <h3>图层</h3>
  255. <div class="layer-actions">
  256. <el-button size="small" @click="moveLayerUp" :disabled="!canMoveUp">
  257. <el-icon><ArrowUp /></el-icon>
  258. </el-button>
  259. <el-button size="small" @click="moveLayerDown" :disabled="!canMoveDown">
  260. <el-icon><ArrowDown /></el-icon>
  261. </el-button>
  262. <el-button size="small" type="danger" @click="deleteLayer" :disabled="!selectedLayerId">
  263. <el-icon><Delete /></el-icon>
  264. </el-button>
  265. </div>
  266. <el-divider />
  267. <div class="layer-list" ref="layerListRef">
  268. <div
  269. v-for="(layer, index) in reversedLayers"
  270. :key="layer.id"
  271. class="layer-item"
  272. :class="{
  273. 'selected': selectedLayerId === layer.id,
  274. 'dragging': dragState.draggingId === layer.id,
  275. 'drag-over': dragState.dragOverId === layer.id,
  276. 'locked': layer.locked
  277. }"
  278. :draggable="true"
  279. @click="selectLayer(layer.id)"
  280. @dragstart="handleDragStart($event, layer)"
  281. @dragover="handleDragOver($event, layer)"
  282. @drop="handleDrop($event, layer)"
  283. @dragend="handleDragEnd"
  284. @dragenter="handleDragEnter($event, layer)"
  285. @dragleave="handleDragLeave"
  286. >
  287. <el-icon class="drag-handle">
  288. <Rank />
  289. </el-icon>
  290. <el-icon class="visibility-icon" @click.stop="toggleLayerVisibility(layer.id)">
  291. <View v-if="layer.visible" />
  292. <Hide v-else />
  293. </el-icon>
  294. <el-icon class="lock-icon" @click.stop="toggleLayerLock(layer.id)">
  295. <Lock v-if="layer.locked" />
  296. <Unlock v-else />
  297. </el-icon>
  298. <template v-if="layer.type === 'text'">
  299. <div class="layer-thumbnail text-thumbnail">
  300. <el-icon><Document /></el-icon>
  301. </div>
  302. </template>
  303. <template v-else>
  304. <img :src="layer.url" class="layer-thumbnail" />
  305. </template>
  306. <span class="layer-name">{{ layer.name }}</span>
  307. </div>
  308. <div v-if="layers.length === 0" class="empty-tip">
  309. 暂无图层,请上传素材
  310. </div>
  311. </div>
  312. </div>
  313. </div>
  314. </template>
  315. <script setup>
  316. import { ref, computed, reactive, onMounted, onUnmounted } from 'vue'
  317. import { ElMessage } from 'element-plus'
  318. import { Pointer, Rank, ArrowUp, ArrowDown, Delete, View, Hide, Lock, Unlock, Plus, Document, Picture, Refresh, Upload } from '@element-plus/icons-vue'
  319. import { Material_List } from '@/api/mes/job'
  320. const canvasRef = ref(null)
  321. const canvasAreaRef = ref(null)
  322. const layerListRef = ref(null)
  323. const currentTool = ref('select')
  324. const activeTab = ref('design') // 'design' 或 'material'
  325. const canvasWidth = ref(600)
  326. const canvasHeight = ref(450)
  327. const canvasRatio = ref('4:3')
  328. const zoomLevel = ref(100)
  329. const layers = ref([])
  330. const selectedLayerId = ref(null)
  331. const maintainAspectRatio = ref(true) // 默认启用等比例缩放
  332. // 拖拽状态
  333. const dragState = reactive({
  334. draggingId: null,
  335. dragOverId: null,
  336. draggedLayer: null
  337. })
  338. // 画布比例配置 - 使用更小的默认尺寸以适应屏幕
  339. const ratioConfig = {
  340. '1:1': { width: 500, height: 500, ratio: 1 },
  341. '4:3': { width: 600, height: 450, ratio: 4/3 },
  342. '3:4': { width: 450, height: 600, ratio: 3/4 },
  343. '16:9': { width: 640, height: 360, ratio: 16/9 },
  344. '9:16': { width: 360, height: 640, ratio: 9/16 },
  345. '3:2': { width: 600, height: 400, ratio: 3/2 },
  346. '2:3': { width: 400, height: 600, ratio: 2/3 }
  347. }
  348. const selectedLayer = computed(() => {
  349. return layers.value.find(l => l.id === selectedLayerId.value) || null
  350. })
  351. const reversedLayers = computed(() => {
  352. return [...layers.value].reverse()
  353. })
  354. const selectedLayerIndex = computed(() => {
  355. return layers.value.findIndex(l => l.id === selectedLayerId.value)
  356. })
  357. const canMoveUp = computed(() => {
  358. return selectedLayerId.value && selectedLayerIndex.value < layers.value.length - 1
  359. })
  360. const canMoveDown = computed(() => {
  361. return selectedLayerId.value && selectedLayerIndex.value > 0
  362. })
  363. let layerIdCounter = 0
  364. const beforeUpload = (file) => {
  365. const isImage = file.type.startsWith('image/')
  366. const isLt10M = file.size / 1024 / 1024 < 10
  367. if (!isImage) {
  368. ElMessage.error('只能上传图片文件!')
  369. return false
  370. }
  371. if (!isLt10M) {
  372. ElMessage.error('图片大小不能超过 10MB!')
  373. return false
  374. }
  375. return true
  376. }
  377. const handleUpload = (options) => {
  378. const { file } = options
  379. const reader = new FileReader()
  380. reader.onload = (e) => {
  381. const img = new Image()
  382. img.onload = () => {
  383. const maxSize = 300
  384. let width = img.width
  385. let height = img.height
  386. if (width > maxSize || height > maxSize) {
  387. const ratio = Math.min(maxSize / width, maxSize / height)
  388. width = width * ratio
  389. height = height * ratio
  390. }
  391. const newLayer = {
  392. id: ++layerIdCounter,
  393. name: file.name,
  394. url: e.target.result,
  395. x: Math.round((canvasWidth.value - width) / 2),
  396. y: Math.round((canvasHeight.value - height) / 2),
  397. width: Math.round(width),
  398. height: Math.round(height),
  399. rotation: 0,
  400. opacity: 100,
  401. visible: true,
  402. locked: false,
  403. originalWidth: img.width,
  404. originalHeight: img.height
  405. }
  406. layers.value.push(newLayer)
  407. selectedLayerId.value = newLayer.id
  408. ElMessage.success('素材上传成功!')
  409. }
  410. img.src = e.target.result
  411. }
  412. reader.readAsDataURL(file)
  413. }
  414. // 添加文字图层
  415. const addTextLayer = () => {
  416. const newLayer = {
  417. id: ++layerIdCounter,
  418. name: '文字 ' + (textLayerCount.value + 1),
  419. type: 'text',
  420. text: '双击编辑文字',
  421. x: canvasWidth.value / 2 - 50,
  422. y: canvasHeight.value / 2 - 15,
  423. width: 100,
  424. height: 30,
  425. rotation: 0,
  426. opacity: 100,
  427. visible: true,
  428. locked: false,
  429. fontSize: 16,
  430. fontFamily: 'Arial',
  431. fontWeight: 'normal',
  432. fontStyle: 'normal',
  433. textDecoration: 'none',
  434. color: '#000000',
  435. textAlign: 'left',
  436. backgroundColor: 'transparent',
  437. lineHeight: 1.5,
  438. letterSpacing: 0
  439. }
  440. layers.value.push(newLayer)
  441. selectedLayerId.value = newLayer.id
  442. textLayerCount.value++
  443. ElMessage.success('文字图层已添加')
  444. }
  445. const textLayerCount = ref(0)
  446. // 素材库状态
  447. const materials = ref([])
  448. const materialsLoading = ref(false)
  449. // 获取素材库数据
  450. const fetchMaterials = async () => {
  451. console.log('fetchMaterials called')
  452. try {
  453. materialsLoading.value = true
  454. console.log('开始获取素材库数据')
  455. const response = await Material_List()
  456. console.log('素材库数据:', response)
  457. if (response.code === 0) {
  458. materials.value = response.data
  459. console.log('素材库数据更新成功:', materials.value)
  460. } else {
  461. console.error('获取素材库失败:', response)
  462. ElMessage.error('获取素材库失败')
  463. }
  464. } catch (error) {
  465. console.error('获取素材库错误:', error)
  466. ElMessage.error('获取素材库失败')
  467. } finally {
  468. materialsLoading.value = false
  469. console.log('获取素材库完成')
  470. }
  471. }
  472. // 标签页点击事件
  473. const handleTabClick = (tab) => {
  474. console.log('Tab clicked:', tab.props.name)
  475. if (tab.props.name === 'material') {
  476. fetchMaterials()
  477. }
  478. }
  479. // 点击素材添加到画布
  480. const addMaterialToCanvas = (material) => {
  481. const img = new Image()
  482. img.onload = function() {
  483. let width = img.width
  484. let height = img.height
  485. const maxSize = 200
  486. if (width > maxSize || height > maxSize) {
  487. const ratio = Math.min(maxSize / width, maxSize / height)
  488. width = width * ratio
  489. height = height * ratio
  490. }
  491. const newLayer = {
  492. id: ++layerIdCounter,
  493. name: `素材 ${layerIdCounter}`,
  494. url: material.material_url,
  495. x: Math.round((canvasWidth.value - width) / 2),
  496. y: Math.round((canvasHeight.value - height) / 2),
  497. width: Math.round(width),
  498. height: Math.round(height),
  499. rotation: 0,
  500. opacity: 100,
  501. visible: true,
  502. locked: false,
  503. originalWidth: img.width,
  504. originalHeight: img.height
  505. }
  506. layers.value.push(newLayer)
  507. selectedLayerId.value = newLayer.id
  508. ElMessage.success('素材已添加到画布!')
  509. }
  510. img.src = material.material_url
  511. }
  512. // 组件挂载时获取素材库数据
  513. // 全局点击事件处理函数:仅在中间画布区域内点击空白处时取消选中
  514. const handleGlobalClick = (e) => {
  515. const areaEl = canvasAreaRef.value
  516. if (!areaEl) return
  517. // 是否在中间画布区域内点击
  518. const isInCanvasArea = areaEl.contains(e.target)
  519. // 检查是否点击了图层
  520. const isLayerClick = e.target.closest('.layer')
  521. // 检查是否点击了画布
  522. const isCanvasClick = e.target === canvasRef.value || canvasRef.value?.contains(e.target)
  523. // 只有在中间画布区域内,并且既没有点击图层也没有点击画布,才取消选择
  524. if (isInCanvasArea && !isLayerClick && !isCanvasClick) {
  525. selectedLayerId.value = null
  526. }
  527. }
  528. onMounted(() => {
  529. fetchMaterials()
  530. // 添加全局点击事件监听器
  531. document.addEventListener('click', handleGlobalClick)
  532. })
  533. onUnmounted(() => {
  534. // 移除全局点击事件监听器
  535. document.removeEventListener('click', handleGlobalClick)
  536. })
  537. // 保存模版
  538. const saveTemplate = () => {
  539. // 构建模版数据
  540. const templateData = {
  541. canvasWidth: canvasWidth.value,
  542. canvasHeight: canvasHeight.value,
  543. canvasRatio: canvasRatio.value,
  544. layers: layers.value.map(layer => ({
  545. id: layer.id,
  546. name: layer.name,
  547. type: layer.type || 'image',
  548. url: layer.url,
  549. text: layer.text,
  550. x: layer.x,
  551. y: layer.y,
  552. width: layer.width,
  553. height: layer.height,
  554. rotation: layer.rotation,
  555. opacity: layer.opacity,
  556. visible: layer.visible,
  557. locked: layer.locked,
  558. fontFamily: layer.fontFamily,
  559. fontSize: layer.fontSize,
  560. color: layer.color,
  561. backgroundColor: layer.backgroundColor,
  562. fontWeight: layer.fontWeight,
  563. fontStyle: layer.fontStyle,
  564. textDecoration: layer.textDecoration,
  565. lineHeight: layer.lineHeight,
  566. letterSpacing: layer.letterSpacing,
  567. textAlign: layer.textAlign,
  568. originalWidth: layer.originalWidth,
  569. originalHeight: layer.originalHeight
  570. }))
  571. }
  572. // 这里可以根据实际需求发送请求到后端保存模版
  573. // 暂时使用本地存储示例
  574. console.log('保存模版数据:', templateData)
  575. // 示例: 保存到本地存储
  576. localStorage.setItem('templateDesign', JSON.stringify(templateData))
  577. ElMessage.success('模版保存成功!')
  578. }
  579. const getLayerStyle = (layer) => {
  580. if (!layer.visible) {
  581. return { display: 'none' }
  582. }
  583. return {
  584. left: layer.x + 'px',
  585. top: layer.y + 'px',
  586. width: layer.type === 'text' ? 'auto' : layer.width + 'px',
  587. height: layer.type === 'text' ? 'auto' : layer.height + 'px',
  588. transform: `rotate(${layer.rotation}deg)`,
  589. opacity: layer.opacity / 100
  590. }
  591. }
  592. // 获取文字图层样式
  593. const getTextStyle = (layer) => {
  594. return {
  595. fontSize: layer.fontSize + 'px',
  596. fontFamily: layer.fontFamily,
  597. fontWeight: layer.fontWeight,
  598. fontStyle: layer.fontStyle,
  599. textDecoration: layer.textDecoration,
  600. color: layer.color,
  601. textAlign: layer.textAlign,
  602. backgroundColor: layer.backgroundColor,
  603. lineHeight: layer.lineHeight,
  604. letterSpacing: layer.letterSpacing + 'px',
  605. padding: '4px 8px',
  606. whiteSpace: 'nowrap',
  607. userSelect: 'none'
  608. }
  609. }
  610. // 双击编辑文字
  611. const editingTextLayer = ref(null)
  612. const handleLayerDblClick = (e, layer) => {
  613. if (layer.type === 'text' && !layer.locked) {
  614. editingTextLayer.value = layer
  615. // 创建输入框进行编辑
  616. const input = document.createElement('input')
  617. input.value = layer.text
  618. input.style.cssText = `
  619. position: absolute;
  620. left: ${layer.x}px;
  621. top: ${layer.y}px;
  622. font-size: ${layer.fontSize}px;
  623. font-family: ${layer.fontFamily};
  624. font-weight: ${layer.fontWeight};
  625. font-style: ${layer.fontStyle};
  626. color: ${layer.color};
  627. background: white;
  628. border: 2px solid #409eff;
  629. padding: 4px 8px;
  630. z-index: 1000;
  631. outline: none;
  632. `
  633. const canvas = canvasRef.value
  634. canvas.appendChild(input)
  635. input.focus()
  636. input.select()
  637. const saveEdit = () => {
  638. layer.text = input.value || '双击编辑文字'
  639. canvas.removeChild(input)
  640. editingTextLayer.value = null
  641. }
  642. input.addEventListener('blur', saveEdit)
  643. input.addEventListener('keydown', (e) => {
  644. if (e.key === 'Enter') {
  645. saveEdit()
  646. }
  647. })
  648. }
  649. }
  650. // 画布比例变化处理
  651. const handleCanvasRatioChange = (ratio) => {
  652. if (ratio === 'custom') return
  653. const config = ratioConfig[ratio]
  654. if (config) {
  655. canvasWidth.value = config.width
  656. canvasHeight.value = config.height
  657. ElMessage.success(`画布尺寸已设置为 ${ratio}`)
  658. }
  659. }
  660. // 画布尺寸变化处理
  661. const handleCanvasSizeChange = () => {
  662. // 当手动调整尺寸时,重置比例为自定义
  663. if (canvasRatio.value !== 'custom') {
  664. const config = ratioConfig[canvasRatio.value]
  665. if (config) {
  666. const currentRatio = canvasWidth.value / canvasHeight.value
  667. const expectedRatio = config.ratio
  668. // 如果比例偏差较大,切换到自定义
  669. if (Math.abs(currentRatio - expectedRatio) > 0.1) {
  670. canvasRatio.value = 'custom'
  671. }
  672. }
  673. }
  674. }
  675. // 图片宽度变化时,如果锁定比例,自动调整高度
  676. const handleWidthChange = (newWidth) => {
  677. if (!maintainAspectRatio.value || !selectedLayer.value) return
  678. const layer = selectedLayer.value
  679. const aspectRatio = layer.originalWidth / layer.originalHeight
  680. layer.height = Math.round(newWidth / aspectRatio)
  681. }
  682. // 图片高度变化时,如果锁定比例,自动调整宽度
  683. const handleHeightChange = (newHeight) => {
  684. if (!maintainAspectRatio.value || !selectedLayer.value) return
  685. const layer = selectedLayer.value
  686. const aspectRatio = layer.originalWidth / layer.originalHeight
  687. layer.width = Math.round(newHeight * aspectRatio)
  688. }
  689. const selectLayer = (id) => {
  690. selectedLayerId.value = id
  691. }
  692. const toggleLayerVisibility = (id) => {
  693. const layer = layers.value.find(l => l.id === id)
  694. if (layer) {
  695. layer.visible = !layer.visible
  696. }
  697. }
  698. const toggleLayerLock = (id) => {
  699. const layer = layers.value.find(l => l.id === id)
  700. if (layer) {
  701. layer.locked = !layer.locked
  702. ElMessage.success(layer.locked ? '图层已锁定' : '图层已解锁')
  703. }
  704. }
  705. const moveLayerUp = () => {
  706. const index = selectedLayerIndex.value
  707. if (index < layers.value.length - 1) {
  708. const temp = layers.value[index]
  709. layers.value[index] = layers.value[index + 1]
  710. layers.value[index + 1] = temp
  711. }
  712. }
  713. const moveLayerDown = () => {
  714. const index = selectedLayerIndex.value
  715. if (index > 0) {
  716. const temp = layers.value[index]
  717. layers.value[index] = layers.value[index - 1]
  718. layers.value[index - 1] = temp
  719. }
  720. }
  721. // 拖拽排序相关函数
  722. const handleDragStart = (e, layer) => {
  723. // 如果图层被锁定,禁止拖拽排序
  724. if (layer.locked) {
  725. e.preventDefault()
  726. ElMessage.warning('图层已锁定,无法排序')
  727. return
  728. }
  729. dragState.draggingId = layer.id
  730. dragState.draggedLayer = layer
  731. e.dataTransfer.effectAllowed = 'move'
  732. e.dataTransfer.setData('text/plain', layer.id)
  733. // 设置拖拽时的半透明效果
  734. e.target.style.opacity = '0.5'
  735. }
  736. const handleDragOver = (e, layer) => {
  737. e.preventDefault()
  738. e.dataTransfer.dropEffect = 'move'
  739. }
  740. const handleDragEnter = (e, layer) => {
  741. e.preventDefault()
  742. if (dragState.draggingId !== layer.id) {
  743. dragState.dragOverId = layer.id
  744. }
  745. }
  746. const handleDragLeave = (e) => {
  747. // 只有当离开当前元素时才清除dragOverId
  748. if (!e.currentTarget.contains(e.relatedTarget)) {
  749. dragState.dragOverId = null
  750. }
  751. }
  752. const handleDrop = (e, targetLayer) => {
  753. e.preventDefault()
  754. e.stopPropagation()
  755. const draggedId = dragState.draggingId
  756. const targetId = targetLayer.id
  757. if (draggedId && draggedId !== targetId) {
  758. // 获取原始索引(在layers数组中的索引,不是reversedLayers)
  759. const draggedIndex = layers.value.findIndex(l => l.id === draggedId)
  760. const targetIndex = layers.value.findIndex(l => l.id === targetId)
  761. if (draggedIndex !== -1 && targetIndex !== -1) {
  762. // 移动图层
  763. const [movedLayer] = layers.value.splice(draggedIndex, 1)
  764. layers.value.splice(targetIndex, 0, movedLayer)
  765. ElMessage.success('图层排序已更新')
  766. }
  767. }
  768. // 重置拖拽状态
  769. dragState.dragOverId = null
  770. }
  771. const handleDragEnd = (e) => {
  772. // 恢复透明度
  773. if (e.target) {
  774. e.target.style.opacity = '1'
  775. }
  776. // 重置所有拖拽状态
  777. dragState.draggingId = null
  778. dragState.dragOverId = null
  779. dragState.draggedLayer = null
  780. }
  781. const deleteLayer = () => {
  782. const index = selectedLayerIndex.value
  783. if (index !== -1) {
  784. layers.value.splice(index, 1)
  785. selectedLayerId.value = layers.value.length > 0 ? layers.value[0].id : null
  786. ElMessage.success('图层已删除')
  787. }
  788. }
  789. let isDragging = false
  790. let isResizing = false
  791. let isRotating = false
  792. let dragStartX = 0
  793. let dragStartY = 0
  794. let layerStartX = 0
  795. let layerStartY = 0
  796. let resizeDirection = ''
  797. let startWidth = 0
  798. let startHeight = 0
  799. let startRotation = 0
  800. let centerX = 0
  801. let centerY = 0
  802. const handleLayerMouseDown = (e, layer) => {
  803. if (currentTool.value !== 'select' && currentTool.value !== 'move') return
  804. // 如果图层被锁定,禁止拖拽
  805. if (layer.locked) {
  806. selectedLayerId.value = layer.id
  807. ElMessage.warning('图层已锁定,无法移动')
  808. return
  809. }
  810. selectedLayerId.value = layer.id
  811. isDragging = true
  812. dragStartX = e.clientX
  813. dragStartY = e.clientY
  814. layerStartX = layer.x
  815. layerStartY = layer.y
  816. e.preventDefault()
  817. }
  818. const handleCanvasMouseDown = (e) => {
  819. // 确保 canvasRef 已经初始化
  820. if (canvasRef.value && e.target === canvasRef.value) {
  821. selectedLayerId.value = null
  822. }
  823. }
  824. const handleCanvasMouseMove = (e) => {
  825. if (!selectedLayer.value) return
  826. if (isDragging) {
  827. const deltaX = e.clientX - dragStartX
  828. const deltaY = e.clientY - dragStartY
  829. selectedLayer.value.x = Math.round(layerStartX + deltaX)
  830. selectedLayer.value.y = Math.round(layerStartY + deltaY)
  831. }
  832. if (isResizing) {
  833. const deltaX = e.clientX - dragStartX
  834. const deltaY = e.clientY - dragStartY
  835. const layer = selectedLayer.value
  836. const aspectRatio = layer.originalWidth / layer.originalHeight
  837. let newWidth = startWidth
  838. let newHeight = startHeight
  839. // 计算新的宽度和高度
  840. if (resizeDirection.includes('e')) {
  841. newWidth = Math.max(20, startWidth + deltaX)
  842. }
  843. if (resizeDirection.includes('w')) {
  844. newWidth = Math.max(20, startWidth - deltaX)
  845. }
  846. if (resizeDirection.includes('s')) {
  847. newHeight = Math.max(20, startHeight + deltaY)
  848. }
  849. if (resizeDirection.includes('n')) {
  850. newHeight = Math.max(20, startHeight - deltaY)
  851. }
  852. // 如果锁定等比例,根据宽高比调整
  853. if (maintainAspectRatio.value) {
  854. if (resizeDirection === 'se' || resizeDirection === 'nw') {
  855. // 对角线调整,以宽度为准
  856. newHeight = newWidth / aspectRatio
  857. } else if (resizeDirection === 'ne' || resizeDirection === 'sw') {
  858. // 对角线调整,以宽度为准
  859. newHeight = newWidth / aspectRatio
  860. } else if (resizeDirection.includes('e') || resizeDirection.includes('w')) {
  861. // 水平调整
  862. newHeight = newWidth / aspectRatio
  863. } else if (resizeDirection.includes('n') || resizeDirection.includes('s')) {
  864. // 垂直调整
  865. newWidth = newHeight * aspectRatio
  866. }
  867. }
  868. // 应用新的尺寸和位置
  869. if (resizeDirection.includes('w')) {
  870. layer.x = Math.round(layerStartX + (startWidth - newWidth))
  871. }
  872. if (resizeDirection.includes('n')) {
  873. layer.y = Math.round(layerStartY + (startHeight - newHeight))
  874. }
  875. layer.width = Math.round(newWidth)
  876. layer.height = Math.round(newHeight)
  877. }
  878. if (isRotating) {
  879. const rect = canvasRef.value.getBoundingClientRect()
  880. const mouseX = e.clientX - rect.left
  881. const mouseY = e.clientY - rect.top
  882. const angle = Math.atan2(mouseY - centerY, mouseX - centerX) * 180 / Math.PI
  883. selectedLayer.value.rotation = Math.round(angle + 90)
  884. }
  885. }
  886. const handleCanvasMouseUp = () => {
  887. isDragging = false
  888. isResizing = false
  889. isRotating = false
  890. }
  891. const startResize = (e, direction) => {
  892. if (!selectedLayer.value) return
  893. isResizing = true
  894. resizeDirection = direction
  895. dragStartX = e.clientX
  896. dragStartY = e.clientY
  897. startWidth = selectedLayer.value.width
  898. startHeight = selectedLayer.value.height
  899. layerStartX = selectedLayer.value.x
  900. layerStartY = selectedLayer.value.y
  901. }
  902. const startRotate = (e) => {
  903. if (!selectedLayer.value) return
  904. isRotating = true
  905. const rect = canvasRef.value.getBoundingClientRect()
  906. centerX = selectedLayer.value.x + selectedLayer.value.width / 2
  907. centerY = selectedLayer.value.y + selectedLayer.value.height / 2
  908. startRotation = selectedLayer.value.rotation
  909. }
  910. const handleCanvasWheel = (e) => {
  911. e.preventDefault()
  912. const delta = e.deltaY > 0 ? -10 : 10
  913. zoomLevel.value = Math.max(10, Math.min(200, zoomLevel.value + delta))
  914. }
  915. </script>
  916. <style scoped>
  917. .template-design-container {
  918. display: flex;
  919. height: calc(100vh - 140px);
  920. background-color: #f5f5f5;
  921. }
  922. .toolbar {
  923. width: 300px;
  924. background-color: #fff;
  925. border-right: 1px solid #ddd;
  926. padding: 8px 12px;
  927. display: flex;
  928. flex-direction: column;
  929. box-sizing: border-box;
  930. overflow: hidden; /* 外层高度锁死,内部区域单独滚动 */
  931. }
  932. .toolbar h3 {
  933. margin: 0 0 8px 0;
  934. font-size: 14px;
  935. color: #333;
  936. flex-shrink: 0;
  937. }
  938. .toolbar-tabs {
  939. flex: 1;
  940. min-height: 0;
  941. display: flex;
  942. flex-direction: column;
  943. }
  944. .toolbar-tabs :deep(.el-tabs__header) {
  945. flex-shrink: 0;
  946. margin-bottom: 8px;
  947. }
  948. .toolbar-tabs :deep(.el-tabs__content) {
  949. flex: 1;
  950. min-height: 0;
  951. overflow-y: auto;
  952. padding: 0;
  953. }
  954. .toolbar-tabs :deep(.el-tab-pane) {
  955. height: 100%;
  956. display: flex;
  957. flex-direction: column;
  958. }
  959. .toolbar-tabs :deep(.material-tab) {
  960. color: red !important;
  961. font-weight: bold;
  962. }
  963. /* 完整素材库样式 */
  964. .materials-library-full {
  965. flex: 1;
  966. display: flex;
  967. flex-direction: column;
  968. height: 100%;
  969. }
  970. .materials-header {
  971. display: flex;
  972. align-items: center;
  973. justify-content: space-between;
  974. margin-bottom: 8px;
  975. }
  976. .materials-header h4 {
  977. margin: 0;
  978. font-size: 12px;
  979. color: #666;
  980. display: flex;
  981. align-items: center;
  982. gap: 4px;
  983. }
  984. .materials-list-full {
  985. flex: 1;
  986. display: grid;
  987. grid-template-columns: repeat(3, 1fr);
  988. gap: 6px;
  989. overflow-y: auto;
  990. padding: 4px;
  991. background-color: #f9f9f9;
  992. border-radius: 4px;
  993. min-height: 0;
  994. }
  995. .material-item-full {
  996. position: relative;
  997. aspect-ratio: 1;
  998. cursor: pointer;
  999. border-radius: 8px;
  1000. overflow: hidden;
  1001. border: 1px solid #e5e5e5;
  1002. transition: all 0.2s;
  1003. }
  1004. .material-item-full:hover {
  1005. border-color: #409eff;
  1006. transform: scale(1.03);
  1007. z-index: 1;
  1008. }
  1009. .material-item-full img {
  1010. width: 100%;
  1011. height: 100%;
  1012. object-fit: cover;
  1013. display: block;
  1014. }
  1015. .material-item-full .material-overlay {
  1016. position: absolute;
  1017. top: 0;
  1018. left: 0;
  1019. right: 0;
  1020. bottom: 0;
  1021. background: rgba(0, 0, 0, 0.6);
  1022. display: flex;
  1023. flex-direction: column;
  1024. align-items: center;
  1025. justify-content: center;
  1026. opacity: 0;
  1027. transition: opacity 0.2s;
  1028. color: white;
  1029. gap: 4px;
  1030. font-size: 12px;
  1031. }
  1032. .material-item-full:hover .material-overlay {
  1033. opacity: 1;
  1034. }
  1035. .material-item-full .material-overlay .el-icon {
  1036. font-size: 24px;
  1037. }
  1038. .tool-buttons {
  1039. display: flex;
  1040. flex-direction: column;
  1041. flex-shrink: 0;
  1042. }
  1043. .canvas-settings {
  1044. flex-shrink: 0;
  1045. }
  1046. .canvas-settings h4,
  1047. .layer-info h4 {
  1048. margin: 0 0 6px 0;
  1049. font-size: 12px;
  1050. color: #666;
  1051. }
  1052. .layer-info {
  1053. flex: 1;
  1054. min-height: 0;
  1055. padding-right: 4px;
  1056. }
  1057. .property-item {
  1058. margin-bottom: 4px;
  1059. display: flex;
  1060. align-items: center;
  1061. gap: 6px;
  1062. }
  1063. .property-item span {
  1064. width: 40px;
  1065. font-size: 11px;
  1066. color: #666;
  1067. flex-shrink: 0;
  1068. }
  1069. .property-item .el-input-number {
  1070. flex: 1;
  1071. min-width: 0;
  1072. }
  1073. .property-item .el-slider {
  1074. flex: 1;
  1075. min-width: 0;
  1076. }
  1077. /* 缩小表单元素 */
  1078. :deep(.el-input-number--small) {
  1079. width: 100% !important;
  1080. }
  1081. :deep(.el-input-number--small .el-input__wrapper) {
  1082. padding: 0 4px;
  1083. }
  1084. :deep(.el-button--small) {
  1085. padding: 4px 8px;
  1086. font-size: 11px;
  1087. }
  1088. :deep(.el-select--small) {
  1089. width: 100%;
  1090. }
  1091. :deep(.el-switch__label) {
  1092. font-size: 11px;
  1093. }
  1094. /* 修复滑块样式 */
  1095. :deep(.el-slider) {
  1096. width: 100%;
  1097. }
  1098. :deep(.el-slider__runway) {
  1099. margin: 8px 0;
  1100. }
  1101. :deep(.el-slider__button-wrapper) {
  1102. z-index: 10;
  1103. }
  1104. .canvas-area {
  1105. flex: 1;
  1106. display: flex;
  1107. flex-direction: column;
  1108. background-color: #e8e8e8;
  1109. overflow: hidden;
  1110. min-width: 0;
  1111. }
  1112. .canvas-wrapper {
  1113. flex: 1;
  1114. display: flex;
  1115. justify-content: center;
  1116. align-items: center;
  1117. overflow: hidden;
  1118. padding: 8px;
  1119. }
  1120. .canvas {
  1121. position: relative;
  1122. background-color: #fff;
  1123. box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  1124. overflow: hidden;
  1125. }
  1126. .layer {
  1127. position: absolute;
  1128. cursor: move;
  1129. user-select: none;
  1130. border: 2px solid transparent;
  1131. transition: border-color 0.2s;
  1132. }
  1133. .layer.selected {
  1134. border-color: #409eff;
  1135. }
  1136. .layer img {
  1137. width: 100%;
  1138. height: 100%;
  1139. display: block;
  1140. pointer-events: none;
  1141. }
  1142. .text-content {
  1143. display: inline-block;
  1144. min-width: 50px;
  1145. cursor: text;
  1146. }
  1147. .text-layer {
  1148. border: 1px dashed #ccc;
  1149. }
  1150. .text-layer.selected {
  1151. border-color: #409eff;
  1152. border-style: solid;
  1153. }
  1154. .resize-handle {
  1155. position: absolute;
  1156. width: 10px;
  1157. height: 10px;
  1158. background-color: #409eff;
  1159. border: 2px solid #fff;
  1160. border-radius: 50%;
  1161. cursor: pointer;
  1162. z-index: 10;
  1163. }
  1164. .resize-handle.nw {
  1165. top: -5px;
  1166. left: -5px;
  1167. cursor: nw-resize;
  1168. }
  1169. .resize-handle.ne {
  1170. top: -5px;
  1171. right: -5px;
  1172. cursor: ne-resize;
  1173. }
  1174. .resize-handle.sw {
  1175. bottom: -5px;
  1176. left: -5px;
  1177. cursor: sw-resize;
  1178. }
  1179. .resize-handle.se {
  1180. bottom: -5px;
  1181. right: -5px;
  1182. cursor: se-resize;
  1183. }
  1184. .rotate-handle {
  1185. position: absolute;
  1186. top: -30px;
  1187. left: 50%;
  1188. transform: translateX(-50%);
  1189. width: 16px;
  1190. height: 16px;
  1191. background-color: #409eff;
  1192. border: 2px solid #fff;
  1193. border-radius: 50%;
  1194. cursor: grab;
  1195. z-index: 10;
  1196. }
  1197. .rotate-handle::before {
  1198. content: '';
  1199. position: absolute;
  1200. top: 100%;
  1201. left: 50%;
  1202. transform: translateX(-50%);
  1203. width: 1px;
  1204. height: 14px;
  1205. background-color: #409eff;
  1206. }
  1207. .layer-panel {
  1208. width: 200px;
  1209. background-color: #fff;
  1210. border-left: 1px solid #ddd;
  1211. padding: 8px 12px;
  1212. display: flex;
  1213. flex-direction: column;
  1214. }
  1215. .layer-panel h3 {
  1216. margin: 0 0 8px 0;
  1217. font-size: 14px;
  1218. color: #333;
  1219. flex-shrink: 0;
  1220. }
  1221. .layer-actions {
  1222. display: flex;
  1223. gap: 6px;
  1224. flex-shrink: 0;
  1225. }
  1226. .layer-list {
  1227. flex: 1;
  1228. margin-top: 6px;
  1229. min-height: 0;
  1230. }
  1231. .layer-item {
  1232. display: flex;
  1233. align-items: center;
  1234. padding: 4px 6px;
  1235. margin-bottom: 3px;
  1236. background-color: #f9f9f9;
  1237. border-radius: 4px;
  1238. cursor: pointer;
  1239. transition: all 0.2s;
  1240. gap: 6px;
  1241. user-select: none;
  1242. }
  1243. .layer-item:hover {
  1244. background-color: #f0f0f0;
  1245. }
  1246. .layer-item.selected {
  1247. background-color: #e6f7ff;
  1248. border: 1px solid #91d5ff;
  1249. }
  1250. .layer-item.dragging {
  1251. opacity: 0.5;
  1252. cursor: grabbing;
  1253. }
  1254. .layer-item.drag-over {
  1255. background-color: #d9ecff;
  1256. border: 2px dashed #409eff;
  1257. transform: scale(1.02);
  1258. }
  1259. .drag-handle {
  1260. font-size: 14px;
  1261. color: #999;
  1262. cursor: grab;
  1263. transition: color 0.2s;
  1264. }
  1265. .drag-handle:hover {
  1266. color: #409eff;
  1267. }
  1268. .layer-item.dragging .drag-handle {
  1269. cursor: grabbing;
  1270. }
  1271. .visibility-icon {
  1272. font-size: 14px;
  1273. color: #666;
  1274. cursor: pointer;
  1275. }
  1276. .visibility-icon:hover {
  1277. color: #409eff;
  1278. }
  1279. .lock-icon {
  1280. font-size: 14px;
  1281. color: #666;
  1282. cursor: pointer;
  1283. transition: color 0.2s;
  1284. }
  1285. .lock-icon:hover {
  1286. color: #f56c6c;
  1287. }
  1288. .layer-item.locked {
  1289. opacity: 0.7;
  1290. }
  1291. .layer-item.locked .drag-handle {
  1292. cursor: not-allowed;
  1293. color: #ccc;
  1294. }
  1295. .layer-thumbnail {
  1296. width: 32px;
  1297. height: 32px;
  1298. object-fit: cover;
  1299. border-radius: 4px;
  1300. border: 1px solid #ddd;
  1301. }
  1302. .text-thumbnail {
  1303. display: flex;
  1304. align-items: center;
  1305. justify-content: center;
  1306. background-color: #f0f0f0;
  1307. color: #666;
  1308. font-size: 16px;
  1309. }
  1310. .layer-name {
  1311. flex: 1;
  1312. font-size: 11px;
  1313. color: #333;
  1314. text-overflow: ellipsis;
  1315. white-space: nowrap;
  1316. }
  1317. .empty-tip {
  1318. text-align: center;
  1319. color: #999;
  1320. padding: 10px;
  1321. font-size: 11px;
  1322. }
  1323. .el-divider {
  1324. margin: 6px 0;
  1325. }
  1326. .save-template-btn {
  1327. width: 100%;
  1328. margin-top: 4px;
  1329. flex-shrink: 0;
  1330. }
  1331. /* 隐藏滚动条但保持功能 */
  1332. .layer-list::-webkit-scrollbar,
  1333. .layer-info::-webkit-scrollbar {
  1334. width: 4px;
  1335. }
  1336. .layer-list::-webkit-scrollbar-thumb,
  1337. .layer-info::-webkit-scrollbar-thumb {
  1338. background-color: #ddd;
  1339. border-radius: 2px;
  1340. }
  1341. .layer-list::-webkit-scrollbar-track,
  1342. .layer-info::-webkit-scrollbar-track {
  1343. background-color: transparent;
  1344. }
  1345. /* 覆盖 admin-box 的 margin-bottom */
  1346. :deep(.admin-box) {
  1347. margin-bottom: 0 !important;
  1348. }
  1349. /* 确保容器占满整个视口 */
  1350. .template-design-container {
  1351. margin: 0 !important;
  1352. padding: 0 !important;
  1353. }
  1354. /* 素材库样式 */
  1355. .materials-library {
  1356. margin: 8px 0;
  1357. }
  1358. .materials-library h4 {
  1359. margin: 0 0 8px 0;
  1360. font-size: 12px;
  1361. color: #333;
  1362. display: flex;
  1363. align-items: center;
  1364. justify-content: space-between;
  1365. }
  1366. .materials-list {
  1367. display: grid;
  1368. grid-template-columns: repeat(2, 1fr);
  1369. gap: 6px;
  1370. max-height: 200px;
  1371. padding: 4px;
  1372. background-color: #f9f9f9;
  1373. border-radius: 4px;
  1374. }
  1375. .material-item {
  1376. position: relative;
  1377. aspect-ratio: 1;
  1378. cursor: pointer;
  1379. border-radius: 4px;
  1380. overflow: hidden;
  1381. border: 1px solid #ddd;
  1382. transition: all 0.2s;
  1383. }
  1384. .material-item:hover {
  1385. border-color: #409eff;
  1386. transform: scale(1.05);
  1387. z-index: 1;
  1388. }
  1389. .material-item img {
  1390. width: 100%;
  1391. height: 100%;
  1392. object-fit: cover;
  1393. display: block;
  1394. }
  1395. .material-overlay {
  1396. position: absolute;
  1397. top: 0;
  1398. left: 0;
  1399. right: 0;
  1400. bottom: 0;
  1401. background: rgba(0, 0, 0, 0.5);
  1402. display: flex;
  1403. align-items: center;
  1404. justify-content: center;
  1405. opacity: 0;
  1406. transition: opacity 0.2s;
  1407. color: white;
  1408. font-size: 20px;
  1409. }
  1410. .material-item:hover .material-overlay {
  1411. opacity: 1;
  1412. }
  1413. .empty-materials {
  1414. grid-column: 1 / -1;
  1415. text-align: center;
  1416. padding: 20px;
  1417. color: #999;
  1418. font-size: 12px;
  1419. }
  1420. .materials-list::-webkit-scrollbar {
  1421. width: 4px;
  1422. }
  1423. .materials-list::-webkit-scrollbar-thumb {
  1424. background-color: #ddd;
  1425. border-radius: 2px;
  1426. }
  1427. .materials-list::-webkit-scrollbar-track {
  1428. background-color: transparent;
  1429. }
  1430. /* 自定义上传样式,改成卡片风格 */
  1431. .custom-upload {
  1432. width: 100%;
  1433. margin-bottom: 8px;
  1434. }
  1435. .custom-upload :deep(.el-upload-dragger) {
  1436. padding: 20px 16px;
  1437. background-color: #ffffff;
  1438. border: 1px dashed #dcdfe6;
  1439. border-radius: 4px;
  1440. display: flex;
  1441. flex-direction: column;
  1442. align-items: center;
  1443. justify-content: center;
  1444. gap: 12px;
  1445. box-sizing: border-box;
  1446. }
  1447. .custom-upload :deep(.upload-main) {
  1448. display: flex;
  1449. align-items: center;
  1450. justify-content: center;
  1451. }
  1452. .custom-upload :deep(.upload-inner-button) {
  1453. display: inline-flex;
  1454. align-items: center;
  1455. justify-content: center;
  1456. padding: 6px 16px;
  1457. border-radius: 999px;
  1458. border: 1px solid #dcdfe6;
  1459. background-color: #ffffff;
  1460. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
  1461. cursor: pointer;
  1462. gap: 4px;
  1463. }
  1464. .custom-upload-icon {
  1465. font-size: 18px;
  1466. color: #409eff;
  1467. }
  1468. .custom-upload :deep(.upload-inner-text) {
  1469. font-size: 13px;
  1470. color: #333333;
  1471. }
  1472. .custom-upload :deep(.el-upload__text) {
  1473. margin: 0;
  1474. font-size: 14px;
  1475. color: #333333;
  1476. }
  1477. .custom-upload :deep(.el-upload__tip) {
  1478. font-size: 12px !important;
  1479. color: #888888 !important;
  1480. text-align: center;
  1481. line-height: 1.4;
  1482. }
  1483. </style>