liuhairui hai 2 semanas
pai
achega
4cd1970d81

+ 36 - 0
src/api/mes/job.js

@@ -1410,3 +1410,39 @@ export const Template_Material_Add = (data) => {
     data
   })
 }
+
+// 更新模版(预览后修正调整并保存)
+export const Template_Material_Update = (data) => {
+  return service({
+    url: '/mes_server/Material/Template_Material_Update',
+    method: 'post',
+    data
+  })
+}
+
+// 删除模版
+export const Template_Material_Delete = (params) => {
+  return service({
+    url: '/mes_server/Material/Template_Material_Delete',
+    method: 'post',
+    params
+  })
+}
+
+// 发布模版(release=1)
+export const Template_Material_Publish = (params) => {
+  return service({
+    url: '/mes_server/Material/Template_Material_Publish',
+    method: 'post',
+    params
+  })
+}
+
+// 取消发布模版(release=0)
+export const Template_Material_Unpublish = (params) => {
+  return service({
+    url: '/mes_server/Material/Template_Material_Unpublish',
+    method: 'post',
+    params
+  })
+}

+ 3385 - 0
src/view/TemplateManagement/CreateTemplate.vue

@@ -0,0 +1,3385 @@
+<template>
+  <div class="template-design-container">
+    <!-- 创建/编辑模版页:左侧工具栏 -->
+    <div class="toolbar">
+      <div class="design-toolbar">
+        <button type="button" class="back-to-records" @click="$emit('back')">
+          <el-icon><ArrowLeft /></el-icon>
+          <span>返回</span>
+        </button>
+        <div class="design-toolbar-name-wrap">
+          <div
+            ref="templateNameElRef"
+            contenteditable="true"
+            class="design-toolbar-name-edit"
+            :data-placeholder="templateName ? '' : '模版名称'"
+            @blur="syncTemplateNameFromEl"
+            @input="enforceTemplateNameMaxLength"
+          ></div>
+          <el-icon class="design-toolbar-name-icon" @click="focusTemplateNameEl"><EditPen /></el-icon>
+        </div>
+      </div>
+      <!-- 左侧竖条:模版设计 / 文字 / 素材库,文字单独一栏 -->
+      <div class="toolbar-body">
+        <nav class="toolbar-vertical-nav">
+          <button type="button" class="nav-item" :class="{ active: activeTab === 'design' }" @click="activeTab = 'design'">
+            <el-icon class="nav-item-icon"><EditPen /></el-icon>
+            <span class="nav-item-text">上传</span>
+          </button>
+          <button type="button" class="nav-item" :class="{ active: activeTab === 'text' }" @click="activeTab = 'text'">
+            <el-icon class="nav-item-icon"><Document /></el-icon>
+            <span class="nav-item-text">文字</span>
+          </button>
+          <button type="button" class="nav-item" :class="{ active: activeTab === 'material' }" @click="activeTab = 'material'; fetchMaterials()">
+            <el-icon class="nav-item-icon"><Box /></el-icon>
+            <span class="nav-item-text">素材库</span>
+          </button>
+          <button type="button" class="nav-item" :class="{ active: activeTab === 'ai' }" @click="activeTab = 'ai'">
+            <el-icon class="nav-item-icon"><MagicStick /></el-icon>
+            <span class="nav-item-text">AI设计</span>
+          </button>
+        </nav>
+        <div class="toolbar-right">
+        <!-- 可滚动区固定高度,切换 tab 时画布不跳动;底部按钮固定 -->
+        <div class="toolbar-tabs-content">
+          <div v-show="activeTab === 'design'" class="toolbar-pane toolbar-pane-scroll">
+          <!-- 操作指引 -->
+          <div class="step-guide">
+            <div class="step-guide-title">操作步骤</div>
+            <div class="step-guide-steps">
+              <div class="step-item"><span class="step-num">1</span> 上传素材图片或从「素材库」选择</div>
+              <div class="step-item"><span class="step-num">2</span> 在「文字」标签中点击 标题/副标题/正文 添加文本</div>
+              <div class="step-item"><span class="step-num">3</span> 在画布上拖拽、调整图层</div>
+              <div class="step-item"><span class="step-num">4</span> 完成设计后 点击底部「生成模版」</div>
+            </div>
+          </div>
+
+          <el-upload
+            class="custom-upload"
+            drag
+            :show-file-list="false"
+            :before-upload="beforeUpload"
+            :http-request="handleUpload"
+            accept="image/png,image/jpg,image/webp"
+            multiple
+          >
+            <div class="upload-main">
+              <div class="upload-inner-button">
+                <el-icon class="custom-upload-icon">
+                  <Upload />
+                </el-icon>
+                <span class="upload-inner-text">上传素材图</span>
+              </div>
+            </div>
+            <div class="el-upload__tip">
+              拖拽或点击上传PNG 10M以内
+            </div>
+          </el-upload>
+          </div>
+          <!-- 文字:单独一栏,点击添加文本 + 标题/副标题/正文 -->
+          <div v-show="activeTab === 'text'" class="toolbar-pane toolbar-pane-scroll text-tab-pane">
+            <p class="text-add-heading">点击添加文本</p>
+            <div class="text-add-btns">
+              <button type="button" class="text-add-btn text-add-btn-title" @click="addTextLayer('title')">标题</button>
+              <button type="button" class="text-add-btn" @click="addTextLayer('subtitle')">副标题</button>
+              <button type="button" class="text-add-btn" @click="addTextLayer('body')">正文</button>
+            </div>
+          </div>
+          <!-- AI 工具:需选中画布中的图片,未选中时提示 -->
+          <div v-show="activeTab === 'ai'" class="toolbar-pane toolbar-pane-scroll ai-tools-pane">
+            <div class="ai-tools-section">
+              <div class="ai-tools-title">图片工具</div>
+              <div
+                v-for="item in aiToolList"
+                :key="item.key"
+                class="ai-tool-item"
+                @click="handleAiToolClick(item)"
+              >
+                <div class="ai-tool-icon" :class="item.iconClass">
+                  <span class="ai-tool-icon-inner">{{ item.iconText }}</span>
+                </div>
+                <div class="ai-tool-body">
+                  <div class="ai-tool-name">{{ item.name }}</div>
+                  <div class="ai-tool-desc">{{ item.desc }}</div>
+                </div>
+              </div>
+            </div>
+          </div>
+          <div v-show="activeTab === 'material'" class="toolbar-pane toolbar-pane-scroll">
+            <div class="materials-panel">
+              <el-input
+                v-model="materialSearch"
+                class="materials-search"
+                size="small"
+                clearable
+                :prefix-icon="Search"
+                placeholder="搜索素材"
+                @input="handleMaterialSearchInput"
+                @clear="handleMaterialSearchInput"
+              />
+
+              <div class="materials-type-chips">
+                <button
+                  type="button"
+                  class="materials-chip"
+                  :class="{ active: activeMaterialType === '全部' }"
+                  @click="setActiveMaterialType('全部')"
+                >
+                  全部
+                </button>
+                <button
+                  v-for="t in materialTypeStats"
+                  :key="t.type"
+                  type="button"
+                  class="materials-chip"
+                  :class="{ active: activeMaterialType === t.type }"
+                  @click="setActiveMaterialType(t.type)"
+                >
+                  {{ t.type }}
+                </button>
+              </div>
+
+              <el-skeleton v-if="materialsLoading" :rows="5" animated />
+
+              <template v-else>
+                <!-- 搜索时:直接展示结果列表 -->
+                <div v-if="materialSearch.trim()" class="materials-list-full">
+                  <div v-if="materialsAfterSearch.length === 0" class="empty-materials">
+                    暂无素材
+                  </div>
+                  <div
+                    v-else
+                    class="material-item-full"
+                    v-for="material in materialsAfterSearch"
+                    :key="material.id"
+                    @click="addMaterialToCanvas(material)"
+                  >
+                    <img :src="resolveMaterialUrl(material.material_url)" :alt="material.id" />
+                    <div class="material-overlay">
+                      <el-icon><Plus /></el-icon>
+                      <span>添加</span>
+                    </div>
+                  </div>
+                </div>
+
+                <!-- 分类明细:点击“更多”或顶部标签进入 -->
+                <div v-else-if="materialViewMode === 'detail'" class="materials-detail">
+                  <div class="materials-detail-header">
+                    <button type="button" class="materials-back" @click="backToMaterialCategory">
+                      <el-icon><ArrowLeft /></el-icon>
+                    </button>
+                    <span class="materials-detail-title">{{ activeMaterialType }}</span>
+                  </div>
+
+                  <div class="materials-list-full">
+                    <div v-if="detailMaterials.length === 0" class="empty-materials">
+                      暂无素材
+                    </div>
+                    <div
+                      v-else
+                      class="material-item-full"
+                      v-for="material in detailMaterials"
+                      :key="material.id"
+                      @click="addMaterialToCanvas(material)"
+                    >
+                      <img :src="resolveMaterialUrl(material.material_url)" :alt="material.id" />
+                      <div class="material-overlay">
+                        <el-icon><Plus /></el-icon>
+                        <span>添加</span>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+
+                <!-- 分类总览:按分类分组展示(每组预览部分 + 更多) -->
+                <div v-else class="materials-groups">
+                  <div
+                    v-for="g in materialTypeStats"
+                    :key="g.type"
+                    class="materials-group"
+                  >
+                    <div class="materials-group-header">
+                      <span class="materials-group-title">{{ g.type }}</span>
+                      <button
+                        v-if="(materialsByType.get(g.type)?.length || 0) > 0"
+                        type="button"
+                        class="materials-more"
+                        @click="openMaterialTypeDetail(g.type)"
+                      >
+                        更多 &gt;
+                      </button>
+                    </div>
+
+                    <div class="materials-list-full materials-list-preview">
+                      <div
+                        class="material-item-full"
+                        v-for="material in (materialsByType.get(g.type) || []).slice(0, PREVIEW_LIMIT)"
+                        :key="material.id"
+                        @click="addMaterialToCanvas(material)"
+                      >
+                        <img :src="resolveMaterialUrl(material.material_url)" :alt="material.id" />
+                        
+                      </div>
+                    </div>
+                  </div>
+                </div>
+              </template>
+            </div>
+          </div>
+        </div>
+        <div class="toolbar-action-footer">
+          <el-button type="primary" class="save-template-btn" @click="saveTemplate">
+            {{ editingTemplateId ? '保存修改' : '生成模版' }}
+          </el-button>
+          <div class="save-template-tip">{{ editingTemplateId ? '修改后点击上方按钮保存,将覆盖原模版' : '完成设计后点击上方按钮生成并保存' }}</div>
+        </div>
+        </div>
+      </div>
+    </div>
+    
+    <!-- 右侧内容区域 -->
+    <div class="content-area">
+      <!-- 中间画布区域 -->
+      <div class="canvas-area" ref="canvasAreaRef">
+        <div class="canvas-wrapper">
+          <div 
+            ref="canvasRef"
+            class="canvas"
+            :style="{
+              width: canvasWidth + 'px',
+              height: canvasHeight + 'px'
+            }"
+            @mousedown="handleCanvasMouseDown"
+            @mousemove="handleCanvasMouseMove"
+            @mouseup="handleCanvasMouseUp"
+            @mouseleave="handleCanvasMouseUp"
+            @wheel="handleCanvasWheel"
+            @contextmenu.prevent
+          >
+            <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">
+        <div class="right-panel-menu">
+          <button type="button" class="right-panel-menu-item" :class="{ active: rightPanelTab === 'layer' }" @click="rightPanelTab = 'layer'">
+            <el-icon><List /></el-icon>
+            <span>图层管理</span>
+          </button>
+          <button type="button" class="right-panel-menu-item" :class="{ active: rightPanelTab === 'props' }" @click="rightPanelTab = 'props'">
+            <el-icon><Setting /></el-icon>
+            <span>调整</span>
+          </button>
+        </div>
+        <!-- 图层管理 -->
+        <div v-show="rightPanelTab === 'layer'" class="right-section">
+          <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>
+          <div class="layer-list" ref="layerListRef">
+            <div
+              v-for="(layer, index) in reversedLayers"
+              :key="layer.id"
+              class="layer-item-card"
+              :class="{
+                'selected': selectedLayerId === layer.id,
+                'dragging': dragState.draggingId === layer.id,
+                'drag-over': dragState.dragOverId === layer.id,
+                'locked': layer.locked
+              }"
+              :draggable="!layer.locked"
+              @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="layer-drag-handle" @click.stop><Rank /></el-icon>
+              <template v-if="layer.type === 'text'">
+                <div class="layer-card-thumb text-thumb">T</div>
+              </template>
+              <template v-else>
+                <img v-if="layer.url" :src="layer.url" class="layer-card-thumb" alt="" />
+                <div v-else class="layer-card-thumb img-placeholder"><el-icon><Picture /></el-icon></div>
+              </template>
+              <span class="layer-card-name">{{ layer.name || (layer.type === 'text' ? '文字' : '图片') }}</span>
+              <el-icon class="layer-card-action" :title="layer.visible ? '隐藏' : '显示'" @click.stop="toggleLayerVisibility(layer.id)">
+                <View v-if="layer.visible" />
+                <Hide v-else />
+              </el-icon>
+              <el-icon class="layer-card-action" :title="layer.locked ? '解锁' : '锁定'" @click.stop="toggleLayerLock(layer.id)">
+                <Lock v-if="layer.locked" />
+                <Unlock v-else />
+              </el-icon>
+              <el-icon class="layer-card-action" title="删除" @click.stop="deleteLayerById(layer.id)">
+                <Delete />
+              </el-icon>
+            </div>
+            <div v-if="layers.length === 0" class="empty-tip">暂无图层,请上传素材</div>
+          </div>
+        </div>
+
+        <!-- 调整:画布尺寸、图层属性、文字属性 全部展开不隐藏 -->
+        <div v-show="rightPanelTab === 'props'" class="right-section right-section-props">
+          <section class="props-block">
+            <h4 class="props-block-title">画布尺寸</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>
+          </section>
+          <section class="props-block">
+            <h4 class="props-block-title">图层属性</h4>
+            <div v-if="!selectedLayer" class="layer-info-empty">选择画布上的图层后可编辑</div>
+            <template v-else>
+              <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>
+              <div v-if="selectedLayer.type !== 'text'" class="property-item">
+                <span>等比例缩放</span>
+                <el-switch v-model="maintainAspectRatio" />
+              </div>
+            </template>
+          </section>
+          <section class="props-block">
+            <h4 class="props-block-title">文字属性</h4>
+            <div v-if="!selectedLayer || selectedLayer.type !== 'text'" class="layer-info-empty">选择文字图层后可编辑</div>
+            <template v-else>
+              <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;" filterable clearable placeholder="搜索字体">
+                  <el-option label="Arial" value="Arial" />
+                  <el-option label="Helvetica" value="Helvetica" />
+                  <el-option label="宋体" value="SimSun" />
+                  <el-option label="黑体" value="SimHei" />
+                  <el-option label="微软雅黑" value="Microsoft YaHei" />
+                  <el-option label="楷体" value="KaiTi" />
+                  <el-option label="仿宋" value="FangSong" />
+                  <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>
+          </section>
+        </div>
+      </div>
+    </div>
+    <!-- AI 工具未选图片时的提示弹窗 -->
+    <el-dialog
+      v-model="aiPromptDialogVisible"
+      :title="null"
+      width="400px"
+      align-center
+      class="ai-prompt-dialog"
+      :show-close="true"
+      @close="aiPromptDialogVisible = false"
+    >
+      <div class="ai-prompt-content">
+        <div class="ai-prompt-icon-wrap">
+          <el-icon class="ai-prompt-icon"><InfoFilled /></el-icon>
+        </div>
+        <p class="ai-prompt-text">使用{{ aiPromptToolName }}前,请在画布模版中先选择一张图片~</p>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, reactive, onMounted, onUnmounted, onActivated, onDeactivated, watch, nextTick } from 'vue'
+import { ElMessage } from 'element-plus'
+import { Rank, ArrowUp, ArrowDown, Delete, View, Hide, Plus, Picture, Upload, Search, ArrowLeft, EditPen, Document, Box, MagicStick, InfoFilled, Lock, Unlock, List, Setting } from '@element-plus/icons-vue'
+import { emitter } from '@/utils/bus.js'
+import { useUserStore } from '@/pinia/modules/user'
+import { Material_List, Template_Material_Add, Template_Material_Update, Template_Material_Relation } from '@/api/mes/job'
+
+const props = defineProps({
+  /** 列表页传入的模版对象(编辑/做同款时必传) */
+  initialTemplate: { type: Object, default: null },
+  /** create=新建, edit=编辑, copy=做同款 */
+  mode: { type: String, default: 'create' }
+})
+const emit = defineEmits(['back'])
+
+const canvasRef = ref(null)
+const canvasAreaRef = ref(null)
+const layerListRef = ref(null)
+const currentTool = ref('select')
+const activeTab = ref('design') // 'design' | 'text' | 'material' | 'ai'
+// 右侧菜单:图层管理 | 调整
+const rightPanelTab = ref('layer')
+// AI 工具:未选图片时提示弹窗
+const aiPromptDialogVisible = ref(false)
+const aiPromptToolName = ref('')
+const aiToolList = [
+  { key: 'cutout', name: '智能抠图', desc: '一键抠图,快速去除背景。', iconClass: 'ai-icon-cutout', iconText: '抠图' },
+  { key: 'hd', name: '变清晰', desc: '超清画质重生,告别渣画质。', iconClass: 'ai-icon-hd', iconText: '清' },
+  { key: 'expand', name: 'AI扩图', desc: '在原图基础上对画面进行拓展。', iconClass: 'ai-icon-expand', iconText: '扩图' },
+  { key: 'generate', name: 'AI生图', desc: '输入文字或参考图,AI根据描述内容生成新的素材图片。', iconClass: 'ai-icon-generate', iconText: '文图' }
+]
+function handleAiToolClick(item) {
+  const layer = selectedLayer.value
+  const isImageLayer = layer && layer.type !== 'text'
+  if (!isImageLayer) {
+    aiPromptToolName.value = item.name
+    aiPromptDialogVisible.value = true
+    return
+  }
+  ElMessage.info('功能开发中')
+}
+function goUploadImage() {
+  aiPromptDialogVisible.value = false
+  activeTab.value = 'design'
+}
+// 本页为创建/编辑页,无 currentView 切换
+const canvasWidth = ref(600)
+const canvasHeight = ref(450)
+const canvasRatio = ref('4:3')
+const zoomLevel = ref(100)
+
+// 当前是否为「预览后编辑」:有值表示正在编辑该 id 的模版,保存时走更新接口
+const editingTemplateId = ref(null)
+// 当前模版名称(设计页顶部 contenteditable 直接编辑,保存时提交 template_name)
+const templateName = ref('未命名模版')
+const templateNameElRef = ref(null)
+const TEMPLATE_NAME_MAX_LEN = 12
+const syncTemplateNameFromEl = () => {
+  const el = templateNameElRef.value
+  if (!el) return
+  const text = (el.textContent || '').trim().slice(0, TEMPLATE_NAME_MAX_LEN)
+  templateName.value = text || '未命名模版'
+  el.textContent = templateName.value
+}
+const enforceTemplateNameMaxLength = () => {
+  const el = templateNameElRef.value
+  if (!el) return
+  const raw = el.textContent || ''
+  if (raw.length <= TEMPLATE_NAME_MAX_LEN) return
+  ElMessage.warning('模版名称最多12个字符')
+  el.textContent = raw.slice(0, TEMPLATE_NAME_MAX_LEN)
+  el.focus()
+  const sel = window.getSelection()
+  if (sel) {
+    const range = document.createRange()
+    range.selectNodeContents(el)
+    range.collapse(false)
+    sel.removeAllRanges()
+    sel.addRange(range)
+  }
+}
+const focusTemplateNameEl = () => {
+  templateNameElRef.value?.focus()
+}
+const setTemplateNameElContent = (value) => {
+  const el = templateNameElRef.value
+  if (el && document.activeElement !== el) {
+    el.textContent = value || '未命名模版'
+  }
+}
+const templatesLoading = ref(false) // 加载模版数据时使用
+
+
+//获取登录用户信息
+const userStore = useUserStore()
+const _username = ref('')
+_username.value = userStore.userInfo.userName + '/' + userStore.userInfo.nickName
+console.log('获取用户信息',_username.value)
+console.log('获取用户名称',userStore.userInfo.nickName)
+
+const layers = ref([])
+const selectedLayerId = ref(null)
+const maintainAspectRatio = ref(false) // 默认不锁定宽高比例,宽高可自由调整
+
+// 拖拽状态
+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
+
+// 将 material_url 转为前端可请求的完整地址(相对路径加根路径,避免图片 404)
+function resolveMaterialUrl(path) {
+  if (!path || typeof path !== 'string') return ''
+  const p = path.trim()
+  if (p.startsWith('http://') || p.startsWith('https://') || p.startsWith('data:')) return p
+  return p.startsWith('/') ? p : '/' + p
+}
+
+// 清空画布,进入新建流程(本组件内部或由 props.mode='create' 触发)
+const clearDesign = () => {
+  editingTemplateId.value = null
+  templateName.value = '未命名模版'
+  layers.value = []
+  selectedLayerId.value = null
+}
+
+// 使用模板:通过模板 id 获取模板关联的图层数据并还原到画布(预览后可编辑并保存修改)
+const useTemplate = async (template) => {
+  if (!template || !template.id) return
+
+  try {
+    templatesLoading.value = true
+    const res = await Template_Material_Relation({ id: template.id })
+
+    if (!res || res.code !== 0) {
+      ElMessage.error(res?.msg || '获取模板数据失败')
+      return
+    }
+
+    const data = res.data
+    let rows = Array.isArray(data) ? data : (data?.layers ?? data?.data ?? [])
+
+    if (!Array.isArray(rows) || !rows.length) {
+      ElMessage.error('暂无图层数据')
+      return
+    }
+
+    // 画布参数:优先用接口返回的,没有则用模板列表项上的
+    const canvasW = data?.canvasWidth ?? template?.canvasWidth
+    const canvasH = data?.canvasHeight ?? template?.canvasHeight
+    const sizeRatio = data?.size ?? template?.size ?? template?.canvasRatio
+
+    if (canvasW != null && canvasW !== '') canvasWidth.value = Number(canvasW) || canvasWidth.value
+    if (canvasH != null && canvasH !== '') canvasHeight.value = Number(canvasH) || canvasHeight.value
+    if (sizeRatio != null && sizeRatio !== '') canvasRatio.value = String(sizeRatio)
+
+    layers.value = rows.map(row => {
+      const width = Number(row.width ?? 100) || 100
+      const height = Number(row.height ?? 100) || 100
+
+      return {
+        id: ++layerIdCounter,
+        name: row.layer_name,
+        type: row.layer_type === 'text' ? 'text' : 'image',
+        url: resolveMaterialUrl(row.material_url || ''),
+        text: row.text_content || '',
+        x: Number(row.position_x ?? 0) || 0,
+        y: Number(row.position_y ?? 0) || 0,
+        width,
+        height,
+        rotation: Number(row.rotation ?? 0) || 0,
+        opacity: Number(row.opacity ?? 100) || 100,
+        visible: String(row.visible ?? '1') !== '0',
+        locked: String(row.locked ?? '0') === '1',
+        fontFamily: row.font_family || 'Arial',
+        fontSize: Number(row.font_size || 16) || 16,
+        color: row.font_color || '#000000',
+        backgroundColor: row.background_color || 'transparent',
+        textAlign: row.text_align || 'left',
+        fontWeight: row.font_weight || 'normal',
+        fontStyle: row.font_style || 'normal',
+        textDecoration: row.font_underline === 'underline' ? 'underline' : 'none',
+        lineHeight: Number(row.line_height || 1.5) || 1.5,
+        letterSpacing: Number(row.letter_spacing || 0) || 0,
+        originalWidth: width,
+        originalHeight: height,
+        zIndex: Number(row.z_index || 0) || 0,
+        materialId: row.material_id,
+        templateId: row.template_id
+      }
+    })
+
+    // 选中顶部图层
+    selectedLayerId.value = layers.value.length ? layers.value[layers.value.length - 1].id : null
+    activeTab.value = 'design'
+    editingTemplateId.value = template.id // 预览后编辑,保存时走更新接口
+    templateName.value = template.template_name || '未命名模版'
+  } catch (e) {
+    console.error('useTemplate error:', e)
+    ElMessage.error('获取模板数据失败')
+  } finally {
+    templatesLoading.value = false
+  }
+}
+
+// 做同款:加载他人模版到画布,清除编辑 id,保存时走新增接口
+const useAsTemplate = async (template) => {
+  if (!template || !template.id) return
+  editingTemplateId.value = null // 避免从「我的作品」编辑后再做同款时仍走保存修改
+  try {
+    templatesLoading.value = true
+    const res = await Template_Material_Relation({ id: template.id })
+    if (!res || res.code !== 0) {
+      ElMessage.error(res?.msg || '获取模板数据失败')
+      return
+    }
+    const data = res.data
+    let rows = Array.isArray(data) ? data : (data?.layers ?? data?.data ?? [])
+    if (!Array.isArray(rows) || !rows.length) {
+      ElMessage.error('暂无图层数据')
+      return
+    }
+    const canvasW = data?.canvasWidth ?? template?.canvasWidth
+    const canvasH = data?.canvasHeight ?? template?.canvasHeight
+    const sizeRatio = data?.size ?? template?.size ?? template?.canvasRatio
+    if (canvasW != null && canvasW !== '') canvasWidth.value = Number(canvasW) || canvasWidth.value
+    if (canvasH != null && canvasH !== '') canvasHeight.value = Number(canvasH) || canvasHeight.value
+    if (sizeRatio != null && sizeRatio !== '') canvasRatio.value = String(sizeRatio)
+    layers.value = rows.map(row => {
+      const width = Number(row.width ?? 100) || 100
+      const height = Number(row.height ?? 100) || 100
+      return {
+        id: ++layerIdCounter,
+        name: row.layer_name,
+        type: row.layer_type === 'text' ? 'text' : 'image',
+        url: resolveMaterialUrl(row.material_url || ''),
+        text: row.text_content || '',
+        x: Number(row.position_x ?? 0) || 0,
+        y: Number(row.position_y ?? 0) || 0,
+        width,
+        height,
+        rotation: Number(row.rotation ?? 0) || 0,
+        opacity: Number(row.opacity ?? 100) || 100,
+        visible: String(row.visible ?? '1') !== '0',
+        locked: String(row.locked ?? '0') === '1',
+        fontFamily: row.font_family || 'Arial',
+        fontSize: Number(row.font_size || 16) || 16,
+        color: row.font_color || '#000000',
+        backgroundColor: row.background_color || 'transparent',
+        textAlign: row.text_align || 'left',
+        fontWeight: row.font_weight || 'normal',
+        fontStyle: row.font_style || 'normal',
+        textDecoration: row.font_underline === 'underline' ? 'underline' : 'none',
+        lineHeight: Number(row.line_height || 1.5) || 1.5,
+        letterSpacing: Number(row.letter_spacing || 0) || 0,
+        originalWidth: width,
+        originalHeight: height,
+        zIndex: Number(row.z_index || 0) || 0,
+        materialId: row.material_id,
+        templateId: row.template_id
+      }
+    })
+    selectedLayerId.value = layers.value.length ? layers.value[layers.value.length - 1].id : null
+    activeTab.value = 'design'
+    templateName.value = template.template_name ? template.template_name + ' (同款)' : '未命名模版'
+    // 不设置 editingTemplateId,保存时走 Template_Material_Add 新增
+  } catch (e) {
+    console.error('useAsTemplate error:', e)
+    ElMessage.error('获取模板数据失败')
+  } finally {
+    templatesLoading.value = false
+  }
+}
+
+// 根据 props 加载:新建 / 编辑 / 做同款
+const loadByMode = () => {
+  const mode = props.mode || 'create'
+  const template = props.initialTemplate
+  if (mode === 'create' || !template) {
+    clearDesign()
+    return
+  }
+  if (mode === 'edit') {
+    useTemplate(template)
+    return
+  }
+  if (mode === 'copy') {
+    useAsTemplate(template)
+  }
+}
+
+watch([() => props.mode, () => props.initialTemplate], loadByMode)
+
+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: Math.round((canvasWidth.value - width) / 2),
+        y: Math.round((canvasHeight.value - height) / 2),
+        width: Math.round(width),
+        height: Math.round(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 TEXT_PRESETS = {
+  title: { fontSize: 32, fontWeight: 'bold', text: '标题文字', name: '标题' },
+  subtitle: { fontSize: 22, fontWeight: 'normal', text: '副标题', name: '副标题' },
+  body: { fontSize: 16, fontWeight: 'normal', text: '正文内容', name: '正文' }
+}
+
+// 添加文字图层,preset 可选 'title' | 'subtitle' | 'body'
+const addTextLayer = (preset) => {
+  const def = preset ? TEXT_PRESETS[preset] : { fontSize: 16, fontWeight: 'normal', text: '双击编辑文字', name: '文字' }
+  const count = textLayerCount.value + 1
+  const name = preset ? `${def.name} ${count}` : '文字 ' + count
+  const newLayer = {
+    id: ++layerIdCounter,
+    name,
+    type: 'text',
+    text: def.text,
+    x: canvasWidth.value / 2 - 50,
+    y: canvasHeight.value / 2 - 15,
+    width: 120,
+    height: Math.max(30, def.fontSize + 8),
+    rotation: 0,
+    opacity: 100,
+    visible: true,
+    locked: false,
+    fontSize: def.fontSize,
+    fontFamily: 'Arial',
+    fontWeight: def.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++
+}
+
+const textLayerCount = ref(0)
+
+// 素材库状态
+const materials = ref([])
+const materialsLoading = ref(false)
+const materialSearch = ref('')
+const activeMaterialType = ref('全部') // 顶部标签选中的分类("全部" / 某个type)
+const materialViewMode = ref('category') // category: 按分类展示;detail: 单分类明细
+
+const PREVIEW_LIMIT = 4
+
+const materialsAfterSearch = computed(() => {
+  // 接口已支持 search,但仍做前端兜底过滤(避免接口不返回预期)
+  const kw = (materialSearch.value || '').trim().toLowerCase()
+  const list = materials.value || []
+  if (!kw) return list
+  return list.filter(m => {
+    const hay = `${m?.type || ''} ${m?.material_url || ''} ${m?.sys_id || ''}`.toLowerCase()
+    return hay.includes(kw)
+  })
+})
+
+// 红框:按 type 汇总 count,并按数量从大到小排序
+const materialTypeStats = computed(() => {
+  const map = new Map()
+  for (const m of materialsAfterSearch.value) {
+    const t = m?.type || '未分类'
+    const c = Number(m?.count ?? 1) || 1
+    map.set(t, (map.get(t) || 0) + c)
+  }
+  return Array.from(map.entries())
+    .map(([type, totalCount]) => ({ type, totalCount }))
+    .sort((a, b) => b.totalCount - a.totalCount)
+})
+
+const materialsByType = computed(() => {
+  const map = new Map()
+  for (const m of materialsAfterSearch.value) {
+    const t = m?.type || '未分类'
+    if (!map.has(t)) map.set(t, [])
+    map.get(t).push(m)
+  }
+  // 每个分类内部也按 count 降序,保证热门在前
+  for (const [t, list] of map.entries()) {
+    list.sort((a, b) => (Number(b?.count ?? 1) || 1) - (Number(a?.count ?? 1) || 1))
+  }
+  return map
+})
+
+const detailMaterials = computed(() => {
+  if (activeMaterialType.value === '全部') return materialsAfterSearch.value
+  return (materialsByType.value.get(activeMaterialType.value) || [])
+})
+
+// 获取素材库数据
+const fetchMaterials = async () => {
+  console.log('fetchMaterials called')
+  try {
+    materialsLoading.value = true
+    console.log('开始获取素材库数据')
+    const response = await Material_List({
+      search: (materialSearch.value || '').trim() || undefined
+    })
+    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('获取素材库完成')
+  }
+}
+
+// 生成画布预览图(base64)
+const generateCanvasPreview = async () => {
+  if (!canvasWidth.value || !canvasHeight.value) return null
+
+  const exportCanvas = document.createElement('canvas')
+  exportCanvas.width = canvasWidth.value
+  exportCanvas.height = canvasHeight.value
+  const ctx = exportCanvas.getContext('2d')
+  if (!ctx) return null
+
+  // 背景填充为白色
+  ctx.fillStyle = '#ffffff'
+  ctx.fillRect(0, 0, exportCanvas.width, exportCanvas.height)
+
+  // 预加载图片图层
+  const imageLayers = layers.value.filter(l => (l.type || 'image') !== 'text' && l.url)
+  await Promise.all(
+    imageLayers.map(layer => {
+      return new Promise(resolve => {
+        const img = new Image()
+        img.crossOrigin = 'anonymous'
+        img.onload = () => {
+          layer._previewImg = img
+          resolve()
+        }
+        img.onerror = () => resolve()
+        img.src = layer.url
+      })
+    })
+  )
+
+  // 按当前顺序绘制所有可见图层
+  for (const layer of layers.value) {
+    if (!layer.visible) continue
+
+    ctx.save()
+    ctx.globalAlpha = (layer.opacity ?? 100) / 100
+
+    const w = layer.width || 0
+    const h = layer.height || 0
+    const cx = (layer.x || 0) + w / 2
+    const cy = (layer.y || 0) + h / 2
+
+    ctx.translate(cx, cy)
+    ctx.rotate(((layer.rotation || 0) * Math.PI) / 180)
+
+    if ((layer.type || 'image') === 'text') {
+      const fontSize = layer.fontSize || 16
+      const fontFamily = layer.fontFamily || 'Arial'
+      const fontWeight = layer.fontWeight || 'normal'
+      const fontStyle = layer.fontStyle || 'normal'
+      ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`
+      ctx.textAlign = layer.textAlign || 'left'
+      ctx.textBaseline = 'top'
+      ctx.fillStyle = layer.color || '#000000'
+
+      const lineHeightPx = (layer.lineHeight || 1.5) * fontSize
+      const lines = (layer.text || '').split('\n')
+
+      let startX = 0
+      if (ctx.textAlign === 'left') {
+        startX = -w / 2 + 4
+      } else if (ctx.textAlign === 'right') {
+        startX = w / 2 - 4
+      } // center 默认 0
+
+      let y = -h / 2 + 4
+      for (const line of lines) {
+        ctx.fillText(line, startX, y)
+        y += lineHeightPx
+      }
+    } else {
+      const img = layer._previewImg
+      if (img) {
+        ctx.drawImage(img, -w / 2, -h / 2, w, h)
+      }
+    }
+
+    ctx.restore()
+  }
+
+  // 清理临时图片引用
+  for (const layer of imageLayers) {
+    delete layer._previewImg
+  }
+
+  return exportCanvas.toDataURL('image/png')
+}
+
+let materialSearchTimer = null
+const handleMaterialSearchInput = () => {
+  if (materialSearchTimer) clearTimeout(materialSearchTimer)
+  materialSearchTimer = setTimeout(() => {
+    fetchMaterials()
+  }, 250)
+}
+
+const setActiveMaterialType = (t) => {
+  activeMaterialType.value = t
+  materialViewMode.value = t === '全部' ? 'category' : 'detail'
+}
+
+const openMaterialTypeDetail = (t) => {
+  activeMaterialType.value = t
+  materialViewMode.value = 'detail'
+}
+
+const backToMaterialCategory = () => {
+  activeMaterialType.value = '全部'
+  materialViewMode.value = 'category'
+}
+
+// 标签页点击事件
+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: resolveMaterialUrl(material.material_url),
+      x: Math.round((canvasWidth.value - width) / 2),
+      y: Math.round((canvasHeight.value - height) / 2),
+      width: Math.round(width),
+      height: Math.round(height),
+      rotation: 0,
+      opacity: 100,
+      visible: true,
+      locked: false,
+      originalWidth: img.width,
+      originalHeight: img.height,
+      materialId: material.id ?? material.material_id ?? null
+    }
+    
+    layers.value.push(newLayer)
+    selectedLayerId.value = newLayer.id
+    // ElMessage.success('素材已添加到画布!')
+  }
+  img.src = resolveMaterialUrl(material.material_url)
+}
+
+// 组件挂载时获取素材库数据
+// 全局点击事件处理函数:仅在中间画布区域内点击空白处时取消选中
+const handleGlobalClick = (e) => {
+  const areaEl = canvasAreaRef.value
+  if (!areaEl) return
+
+  // 是否在中间画布区域内点击
+  const isInCanvasArea = areaEl.contains(e.target)
+  // 检查是否点击了图层
+  const isLayerClick = e.target.closest('.layer')
+  // 检查是否点击了画布
+  const isCanvasClick = e.target === canvasRef.value || canvasRef.value?.contains(e.target)
+
+  // 只有在中间画布区域内,并且既没有点击图层也没有点击画布,才取消选择
+  if (isInCanvasArea && !isLayerClick && !isCanvasClick) {
+    selectedLayerId.value = null
+  }
+}
+
+watch(templateName, (v) => setTemplateNameElContent(v))
+
+onMounted(() => {
+  fetchMaterials()
+  loadByMode()
+  nextTick(() => setTemplateNameElContent(templateName.value))
+  document.addEventListener('click', handleGlobalClick)
+  emitter.emit('templateDesignEnter')
+})
+
+onActivated(() => {
+  emitter.emit('templateDesignEnter')
+})
+
+onDeactivated(() => {
+  emitter.emit('templateDesignLeave')
+})
+
+onUnmounted(() => {
+  document.removeEventListener('click', handleGlobalClick)
+  emitter.emit('templateDesignLeave')
+})
+
+// 保存模版(生成模版)
+const saveTemplate = async () => {
+  if (!layers.value.length) {
+    ElMessage.warning('请先添加图片或文字,再生成模版')
+    return
+  }
+
+  const previewImage = await generateCanvasPreview()
+
+  const templateData = {
+    template_name: templateName.value || '未命名模版',
+    sys_id:userStore.userInfo.nickName,
+    canvasWidth: canvasWidth.value,
+    canvasHeight: canvasHeight.value,
+    canvasRatio: canvasRatio.value,
+    previewImage, // 画布整体预览图(base64)
+    layers: layers.value.map(layer => ({
+      id: layer.id,
+      name: layer.name,
+      type: layer.type || 'image',
+      url: layer.url,
+      text: layer.text,
+      x: layer.x,
+      y: layer.y,
+      width: layer.width,
+      height: layer.height,
+      rotation: layer.rotation,
+      opacity: layer.opacity,
+      visible: layer.visible,
+      locked: layer.locked,
+      material_id: layer.materialId ?? layer.material_id ?? '',
+      fontFamily: layer.fontFamily,
+      fontSize: layer.fontSize,
+      color: layer.color,
+      backgroundColor: layer.backgroundColor,
+      fontWeight: layer.fontWeight,
+      fontStyle: layer.fontStyle,
+      textDecoration: layer.textDecoration,
+      lineHeight: layer.lineHeight,
+      letterSpacing: layer.letterSpacing,
+      textAlign: layer.textAlign,
+      originalWidth: layer.originalWidth,
+      originalHeight: layer.originalHeight
+    }))
+  }
+
+  const isUpdate = !!editingTemplateId.value
+  try {
+    const res = isUpdate
+      ? await Template_Material_Update({ template_id: editingTemplateId.value, ...templateData })
+      : await Template_Material_Add(templateData)
+    if (res && res.code === 0) {
+      ElMessage.success(isUpdate ? '模版已保存修改!' : '模版生成成功!')
+      editingTemplateId.value = null
+      emit('back')
+    } else {
+      ElMessage.error(res?.msg || (isUpdate ? '模版保存失败' : '模版生成失败'))
+    }
+  } catch (e) {
+    console.error(isUpdate ? 'Template_Material_Update error:' : 'Template_Material_Add error:', e)
+    ElMessage.error(isUpdate ? '模版保存失败' : '模版生成失败')
+  }
+}
+
+const getLayerStyle = (layer) => {
+  if (!layer.visible) {
+    return { display: 'none' }
+  }
+  
+  return {
+    left: layer.x + 'px',
+    top: layer.y + 'px',
+    // 文字和图片图层都使用显式宽高,方便通过属性和拖拽进行拉伸
+    width: layer.width + 'px',
+    height: 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: 'pre-wrap',
+    userSelect: 'none'
+  }
+}
+
+// 双击编辑文字
+const editingTextLayer = ref(null)
+
+const handleLayerDblClick = (e, layer) => {
+  if (layer.type === 'text' && !layer.locked) {
+    editingTextLayer.value = layer
+    // 创建多行输入框进行编辑
+    const textarea = document.createElement('textarea')
+    textarea.value = layer.text
+    textarea.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;
+      min-width: 100px;
+      min-height: 30px;
+      resize: none;
+      z-index: 1000;
+      outline: none;
+    `
+    
+    const canvas = canvasRef.value
+    canvas.appendChild(textarea)
+    textarea.focus()
+    textarea.select()
+    
+    const saveEdit = () => {
+      layer.text = textarea.value || '双击编辑文字'
+      canvas.removeChild(textarea)
+      editingTextLayer.value = null
+    }
+    
+    textarea.addEventListener('blur', 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
+  }
+}
+
+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()
+    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)
+    }
+  }
+  
+  // 重置拖拽状态
+  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
+  }
+}
+
+// 按 id 删除图层(用于图层卡片更多菜单)
+const deleteLayerById = (id) => {
+  const index = layers.value.findIndex(l => l.id === id)
+  if (index !== -1) {
+    layers.value.splice(index, 1)
+    selectedLayerId.value = selectedLayerId.value === id ? (layers.value.length > 0 ? layers.value[0].id : null) : selectedLayerId.value
+  }
+}
+
+// 图层卡片更多菜单:锁定/解锁、删除
+const handleLayerMenuCommand = (cmd, layerId) => {
+  if (cmd === 'delete') deleteLayerById(layerId)
+  else if (cmd === 'lock' || cmd === 'unlock') toggleLayerLock(layerId)
+}
+
+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
+    return
+  }
+  
+  selectedLayerId.value = layer.id
+  
+  isDragging = true
+  dragStartX = e.clientX
+  dragStartY = e.clientY
+  layerStartX = layer.x
+  layerStartY = layer.y
+  
+  e.preventDefault()
+}
+
+const handleCanvasMouseDown = (e) => {
+  // 确保 canvasRef 已经初始化
+  if (canvasRef.value && 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;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: stretch;
+  flex: 1 1 0;
+  min-height: 0;
+  min-width: 0;
+  max-width: 100%;
+  box-sizing: border-box;
+  height: 100%;
+  min-height: 100%;
+  background-color: #f5f5f5;
+  overflow: hidden;
+}
+
+/* 左侧栏:与画布同高,固定宽度不收缩 */
+.toolbar {
+  flex: 0 0 370px;
+  width: 370px;
+  min-width: 370px;
+  align-self: stretch;
+  min-height: 100%;
+  background-color: #fff;
+  border-right: 1px solid #ddd;
+  padding: 0;
+  display: flex;
+  flex-direction: column;
+  box-sizing: border-box;
+  overflow: hidden;
+}
+
+.toolbar-body {
+  flex: 1 1 0;
+  min-height: 0;
+  display: flex;
+  overflow: hidden;
+}
+
+/* 左侧竖条:模版设计 / 文字 / 素材库,图标在上、文字在下 */
+.toolbar-vertical-nav {
+  flex-shrink: 0;
+  width: 56px;
+  background: #f5f6f8;
+  border-right: 1px solid #e8e8e8;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 4px;
+}
+
+.toolbar-vertical-nav .nav-item {
+  width: 100%;
+  padding: 10px 6px;
+  font-size: 12px;
+  color: #606266;
+  background: transparent;
+  border: none;
+  cursor: pointer;
+  border-radius: 8px;
+  transition: background 0.2s, color 0.2s;
+  line-height: 1.3;
+  text-align: center;
+  word-break: keep-all;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 4px;
+}
+
+.toolbar-vertical-nav .nav-item-icon {
+  font-size: 20px;
+}
+
+.toolbar-vertical-nav .nav-item-text {
+  font-size: 12px;
+}
+
+.toolbar-vertical-nav .nav-item:hover {
+  background: #e8eaef;
+  color: #303133;
+}
+
+.toolbar-vertical-nav .nav-item.active {
+  background: #ecf5ff;
+  color: #409eff;
+  font-weight: 600;
+}
+
+/* 右侧内容区:可滚动区固定占位 + 底部「生成模版」固定不随内容移动 */
+.toolbar-right {
+  flex: 1 1 0;
+  min-width: 0;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+/* 可滚动区固定高度,切换 tab 时画布区域尺寸不变、不跳动 */
+.toolbar-tabs-content {
+  flex: 1 1 0;
+  min-height: 0;
+  min-width: 0;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  padding: 0;
+}
+
+/* 每个 tab 内容同高、内部滚动,避免切换时布局跳动 */
+.toolbar-pane {
+  flex: 1 1 0;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.toolbar-pane-scroll {
+  overflow-y: auto;
+  overflow-x: hidden;
+  padding: 10px 12px;
+  -webkit-overflow-scrolling: touch;
+}
+
+/* 生成模版按钮固定贴底,不随左侧内容滚动 */
+.toolbar-action-footer {
+  flex: 0 0 auto;
+  padding: 12px 12px 14px;
+  background: #fff;
+  border-top: 1px solid #ebeef5;
+}
+
+.toolbar-action-footer .save-template-btn {
+  width: 100%;
+}
+
+.toolbar-action-footer .save-template-tip {
+  font-size: 11px;
+  color: #909399;
+  margin-top: 6px;
+  line-height: 1.4;
+}
+
+/* AI 工具面板 */
+.ai-tools-pane {
+  padding: 12px 0;
+}
+.ai-tools-section {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+.ai-tools-title {
+  font-size: 14px;
+  font-weight: 600;
+  color: #303133;
+  margin-bottom: 8px;
+  padding: 0 4px;
+}
+.ai-tool-item {
+  display: flex;
+  align-items: flex-start;
+  gap: 12px;
+  padding: 10px 12px;
+  border-radius: 8px;
+  cursor: pointer;
+  transition: background 0.2s;
+}
+.ai-tool-item:hover {
+  background: #f5f7fa;
+}
+.ai-tool-icon {
+  flex-shrink: 0;
+  width: 40px;
+  height: 40px;
+  border-radius: 8px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.ai-tool-icon-inner {
+  font-size: 14px;
+  font-weight: 600;
+  color: #fff;
+}
+.ai-icon-cutout { background: #79bbff; }
+.ai-icon-hd { background: #409eff; }
+.ai-icon-expand { background: #67c23a; }
+.ai-icon-generate { background: #67c23a; }
+.ai-tool-body {
+  flex: 1;
+  min-width: 0;
+}
+.ai-tool-name {
+  font-size: 15px;
+  font-weight: 600;
+  color: #303133;
+  line-height: 1.3;
+}
+.ai-tool-desc {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 4px;
+  line-height: 1.4;
+}
+/* AI 未选图片提示弹窗 */
+.ai-prompt-dialog :deep(.el-dialog__header) {
+  padding: 16px 16px 0;
+}
+.ai-prompt-dialog :deep(.el-dialog__body) {
+  padding: 8px 24px 24px;
+}
+.ai-prompt-content {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  text-align: center;
+}
+.ai-prompt-icon-wrap {
+  width: 48px;
+  height: 48px;
+  border-radius: 50%;
+  background: #ecf5ff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 16px;
+}
+.ai-prompt-icon {
+  font-size: 24px;
+  color: #409eff;
+}
+.ai-prompt-text {
+  margin: 0 0 20px;
+  font-size: 14px;
+  color: #606266;
+  line-height: 1.5;
+}
+.ai-prompt-btn {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+}
+
+/* 完整素材库样式 */
+.materials-panel {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  height: 100%;
+  min-height: 0;
+}
+
+.materials-search :deep(.el-input__wrapper) {
+  border-radius: 16px;
+}
+
+.materials-type-chips {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 6px;
+}
+
+.materials-chip {
+  padding: 4px 10px;
+  border-radius: 14px;
+  border: 1px solid transparent;
+  background: #f3f4f6;
+  color: #606266;
+  font-size: 12px;
+  cursor: pointer;
+  line-height: 1;
+  transition: all 0.15s;
+}
+
+.materials-chip:hover {
+  background: #eef2f7;
+}
+
+.materials-chip.active {
+  background: #ecf5ff;
+  border-color: #b3d8ff;
+  color: #409eff;
+}
+
+.materials-groups {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+
+.materials-group-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 2px 2px 0;
+}
+
+.materials-group-title {
+  font-size: 12px;
+  color: #303133;
+  font-weight: 600;
+}
+
+.materials-more {
+  font-size: 12px;
+  color: #909399;
+  background: transparent;
+  border: none;
+  padding: 0;
+  cursor: pointer;
+}
+
+.materials-more:hover {
+  color: #409eff;
+}
+
+.materials-detail-header {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 2px 2px 0;
+}
+
+.materials-back {
+  width: 26px;
+  height: 26px;
+  border-radius: 50%;
+  border: none;
+  background: #f3f4f6;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+}
+
+.materials-back:hover {
+  background: #eef2f7;
+}
+
+.materials-detail-title {
+  font-size: 13px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.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(3, 1fr);
+  gap: 6px;
+  overflow-y: auto;
+  padding: 4px;
+  background-color: #f9f9f9;
+  border-radius: 4px;
+  min-height: 0;
+}
+
+.materials-list-preview {
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  gap: 6px;
+  padding: 4px;
+  background-color: #f9f9f9;
+  border-radius: 4px;
+}
+
+.material-item-full {
+  position: relative;
+  aspect-ratio: 1;
+  cursor: pointer;
+  border-radius: 8px;
+  overflow: hidden;
+  border: 1px solid #e5e5e5;
+  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;
+}
+
+/* 操作步骤指引:进来就知道怎么做 */
+.step-guide {
+  background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
+  border: 1px solid #bae6fd;
+  border-radius: 8px;
+  padding: 10px 12px;
+  margin-bottom: 12px;
+}
+.step-guide-title {
+  font-size: 12px;
+  font-weight: 600;
+  color: #0369a1;
+  margin-bottom: 8px;
+}
+.step-guide-steps {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+.step-item {
+  font-size: 11px;
+  color: #0c4a6e;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+.step-num {
+  width: 16px;
+  height: 16px;
+  border-radius: 50%;
+  background: #0284c7;
+  color: #fff;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 10px;
+  font-weight: 600;
+  flex-shrink: 0;
+}
+
+.save-template-wrap {
+  flex-shrink: 0;
+  text-align: center;
+}
+.save-template-wrap .save-template-btn {
+  width: 100%;
+}
+.save-template-tip {
+  font-size: 11px;
+  color: #909399;
+  margin-top: 6px;
+  line-height: 1.4;
+}
+
+/* 左侧属性区:三个区块平铺,结构清晰(参考示例项目) */
+.design-sections {
+  flex: 1;
+  min-height: 0;
+  overflow-y: auto;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+.design-section {
+  background: #fafafa;
+  border-radius: 6px;
+  padding: 10px 10px 12px;
+  border: 1px solid #eee;
+}
+/* 文字标签页:点击添加文本 + 标题/副标题/正文,虚线框样式 */
+.text-tab-pane {
+  padding: 16px 12px;
+}
+.text-add-heading {
+  font-size: 13px;
+  color: #909399;
+  margin: 0 0 14px 0;
+}
+.text-add-btns {
+  display: flex;
+  gap: 10px;
+}
+.text-add-btn {
+  flex: 1;
+  padding: 10px 12px;
+  font-size: 14px;
+  color: #303133;
+  background: #fff;
+  border: 1px dashed #dcdfe6;
+  border-radius: 8px;
+  cursor: pointer;
+  transition: background 0.2s, border-color 0.2s;
+}
+.text-add-btn:hover {
+  background: #fafafa;
+  border-color: #c0c4cc;
+}
+.text-add-btn-title {
+  font-weight: 700;
+}
+
+.design-section-title {
+  margin: 0 0 8px 0;
+  font-size: 12px;
+  font-weight: 600;
+  color: #303133;
+  padding-bottom: 6px;
+  border-bottom: 1px solid #ebeef5;
+}
+.layer-info-empty {
+  color: #909399;
+  font-size: 12px;
+  padding: 8px 0;
+  line-height: 1.5;
+}
+.property-item {
+  margin-bottom: 6px;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+.property-item:last-child {
+  margin-bottom: 0;
+}
+.property-item span {
+  width: 60px;
+  font-size: 12px;
+  color: #606266;
+  flex-shrink: 0;
+}
+
+/* 返回按钮区 + 模版命名:固定在左侧栏顶部,预留顶部间距 */
+.design-toolbar {
+  flex-shrink: 0;
+  position: sticky;
+  top: 0;
+  z-index: 10;
+  margin: 0 -12px 0px -12px;
+  padding: 20px 12px 12px;
+  background: #fff;
+  border-bottom: 1px solid #e5e5e5;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+.design-toolbar .design-toolbar-name-wrap {
+  flex: 1;
+  display: flex;
+  justify-content: center;
+  min-width: 0;
+}
+
+.back-to-records {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  gap: 4px;
+  flex-shrink: 0;
+  padding: 6px 10px;
+  font-size: 13px;
+  font-weight: 500;
+  color: #409eff;
+  background: #ecf5ff;
+  border: 1px solid #b3d8ff;
+  cursor: pointer;
+  border-radius: 6px;
+  transition: background 0.2s, border-color 0.2s;
+}
+
+.back-to-records:hover {
+  background: #d9ecff;
+  border-color: #409eff;
+  color: #409eff;
+}
+
+.design-toolbar-name-wrap {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+.design-toolbar-name-edit {
+  min-width: 100px;
+  width: 100%;
+  max-width: 240px;
+  padding: 4px 10px;
+  font-size: 13px;
+  color: #303133;
+  outline: none;
+  border-radius: 4px;
+  white-space: nowrap;
+  overflow: auto;
+  box-sizing: border-box;
+}
+.design-toolbar-name-edit:empty::before {
+  content: attr(data-placeholder);
+  color: #c0c4cc;
+}
+.design-toolbar-name-edit:hover {
+  background: #f5f6f8;
+}
+.design-toolbar-name-edit:focus {
+  background: #fff;
+  box-shadow: 0 0 0 1px #409eff;
+}
+.design-toolbar-name-icon {
+  flex-shrink: 0;
+  font-size: 14px;
+  color: #909399;
+  cursor: pointer;
+}
+.design-toolbar-name-icon:hover {
+  color: #409eff;
+}
+
+.design-toolbar-name-input .el-input__wrapper {
+  padding: 4px 10px;
+  font-size: 13px;
+}
+
+/* 模板库样式 */
+.template-library {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+}
+
+.library-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 8px;
+}
+
+.library-header h3 {
+  margin: 0;
+  font-size: 14px;
+  color: #333;
+}
+
+.template-list {
+  flex: 1;
+  min-height: 0;
+  overflow-y: auto;
+}
+
+.template-grid {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 12px;
+  padding: 8px 0;
+}
+
+.template-item {
+  background-color: #f9f9f9;
+  border: 1px solid #e5e5e5;
+  border-radius: 8px;
+  overflow: hidden;
+  transition: all 0.2s;
+  cursor: pointer;
+}
+
+.template-item:hover {
+  border-color: #409eff;
+  transform: translateY(-2px);
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.template-preview {
+  height: 120px;
+  overflow: hidden;
+}
+
+.template-preview img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.template-info {
+  padding: 8px;
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.template-name {
+  font-size: 12px;
+  color: #333;
+  font-weight: 500;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.template-info .el-button {
+  width: 100%;
+  font-size: 12px;
+  padding: 4px 0;
+}
+
+/* 中间区域:可伸缩,不把左右两侧挤出视口 */
+.content-area {
+  flex: 1 1 0;
+  min-width: 0;
+  min-height: 0;
+  display: flex;
+  background-color: #f5f5f5;
+  overflow: hidden;
+}
+
+.content-area:has(.template-library-view) {
+  flex-direction: column;
+}
+
+/* 模板库视图 */
+.template-library-view {
+  flex: 1;
+  min-width: 0;
+  min-height: 0;
+  padding: 20px 20px 24px 16px;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  background: linear-gradient(180deg, #f0f2f5 0%, #e8eaef 100%);
+}
+
+.template-library-view .library-header {
+  flex-shrink: 0;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 20px;
+  padding: 16px 20px;
+  background: #fff;
+  border-radius: 12px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
+  border: 1px solid rgba(0, 0, 0, 0.04);
+}
+
+.library-header-left {
+  display: flex;
+  align-items: center;
+  gap: 20px;
+}
+
+.library-title {
+  margin: 0;
+  font-size: 18px;
+  font-weight: 600;
+  color: #1a1a2e;
+  letter-spacing: 0.02em;
+}
+
+.btn-start-design {
+  display: inline-flex;
+  align-items: center;
+  gap: 10px;
+  padding: 8px 16px;
+  font-size: 14px;
+  color: #303133;
+  background: #f5f5f5;
+  border: 1px solid #e4e7ed;
+  border-radius: 8px;
+  cursor: pointer;
+  transition: background 0.2s, border-color 0.2s;
+}
+
+.btn-start-design:hover {
+  background: #ebebeb;
+  border-color: #d4d7de;
+}
+
+.btn-start-design-icon {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 24px;
+  height: 24px;
+  border-radius: 50%;
+  border: 1px solid #606266;
+  color: #606266;
+  font-size: 14px;
+}
+
+.btn-start-design-text {
+  font-weight: 500;
+}
+
+.btn-start-design-large {
+  padding: 12px 24px;
+  font-size: 16px;
+}
+
+.btn-start-design-large .btn-start-design-icon {
+  width: 28px;
+  height: 28px;
+  font-size: 16px;
+}
+
+.library-header-actions {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.library-header-actions .library-search {
+  width: 320px;
+}
+
+.library-header-actions .library-search :deep(.el-input__wrapper) {
+  border-radius: 10px;
+  box-shadow: 0 0 0 1px #e4e7ed;
+}
+
+.library-header-actions .library-search :deep(.el-input__wrapper:hover) {
+  box-shadow: 0 0 0 1px #c0c4cc;
+}
+
+/* 菜单栏:我的作品、更多模版,选中项带下划线 */
+.library-menu {
+  flex-shrink: 0;
+  display: flex;
+  align-items: stretch;
+  gap: 0;
+  padding: 0 0 0 4px;
+  margin-bottom: 5px;
+  border-bottom: 1px solid #ebeef5;
+}
+
+.library-menu-item {
+  padding: 12px 20px 14px;
+  font-size: 14px;
+  color: #909399;
+  background: none;
+  border: none;
+  border-bottom: 3px solid transparent;
+  margin-bottom: -1px;
+  border-radius: 0;
+  cursor: pointer;
+  transition: color 0.2s, border-color 0.2s, font-weight 0.2s;
+}
+
+.library-menu-item:hover {
+  color: #409eff;
+}
+
+.library-menu-item.active {
+  color: #409eff;
+  font-weight: 600;
+  border-bottom-color: #409eff;
+}
+
+.template-list-wrap {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  overflow-y: auto;
+  background: #fff;
+  border-radius: 12px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+  padding: 20px 20px 20px 16px;
+  width: 100%;
+  box-sizing: border-box;
+  border: 1px solid rgba(0, 0, 0, 0.04);
+}
+
+.template-list-skeleton {
+  padding: 0;
+}
+
+.records-empty {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  min-height: 360px;
+  padding: 48px 24px;
+  color: #606266;
+  background: linear-gradient(180deg, #fafbfc 0%, #f5f6f8 100%);
+  border-radius: 10px;
+}
+
+.records-empty-icon {
+  color: #c0c4cc;
+  margin-bottom: 20px;
+  opacity: 0.9;
+}
+
+.records-empty-text {
+  margin: 0 0 28px;
+  font-size: 15px;
+  color: #909399;
+}
+
+.template-library-view .template-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, 200px);
+  justify-content: start;
+  gap: 16px;
+  width: 100%;
+}
+
+.template-library-view .template-item {
+  width: 200px;
+  background: #fff;
+  border: 1px solid #e8eaed;
+  border-radius: 12px;
+  overflow: hidden;
+  transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s;
+  cursor: pointer;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
+}
+
+.template-library-view .template-item:hover {
+  border-color: #c6e2ff;
+  transform: translateY(-3px);
+  box-shadow: 0 12px 28px rgba(0, 0, 0, 0.08);
+}
+
+/* 统一预览比例 4:4;图片完整显示、居中,不裁剪 */
+.template-library-view .template-preview {
+  position: relative;
+  width: 100%;
+  height: auto;
+  aspect-ratio: 4 / 4;
+  overflow: hidden;
+  background: #f0f2f5;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.template-library-view .template-preview img {
+  position: absolute;
+  left: 50%;
+  top: 50%;
+  transform: translate(-50%, -50%);
+  max-width: 100%;
+  max-height: 100%;
+  width: auto;
+  height: auto;
+  object-fit: contain;
+  object-position: center center;
+  transition: transform 0.25s ease;
+  display: block;
+}
+
+.template-library-view .template-release-tag {
+  position: absolute;
+  top: 8px;
+  right: 8px;
+  padding: 2px 8px;
+  font-size: 12px;
+  color: #fff;
+  background: rgba(0, 0, 0, 0.5);
+  border-radius: 4px;
+}
+
+.template-library-view .template-release-tag.published {
+  background: rgba(103, 194, 58, 0.85);
+}
+
+.template-library-view .template-item:hover .template-preview img {
+  transform: translate(-50%, -50%) scale(1.02);
+}
+
+/* 图片下方白底操作栏:模版名 + 发布、继续编辑 + 右侧三点 */
+.template-library-view .template-card-footer {
+  padding: 12px 14px;
+  background: #fafbfc;
+  border-top: 1px solid #eee;
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+
+.template-library-view .template-card-name {
+  font-size: 13px;
+  font-weight: 500;
+  color: #303133;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  line-height: 1.3;
+}
+
+.template-library-view .template-card-actions {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+}
+
+.template-library-view .template-card-link {
+  font-size: 13px;
+  background: none;
+  border: none;
+  cursor: pointer;
+  padding: 0;
+  transition: opacity 0.2s, color 0.2s;
+}
+
+.template-library-view .template-card-link-publish {
+  color: #f56c6c;
+}
+
+.template-library-view .template-card-link-publish:hover {
+  color: #f78989;
+}
+
+.template-library-view .template-card-link-publish.disabled,
+.template-library-view .template-card-link-publish:disabled {
+  color: #909399;
+  cursor: default;
+}
+
+.template-library-view .template-card-link-publish.disabled:hover,
+.template-library-view .template-card-link-publish:disabled:hover {
+  color: #909399;
+}
+
+.template-library-view .template-card-link-edit {
+  color: #409eff;
+}
+
+.template-library-view .template-card-link-edit:hover {
+  color: #66b1ff;
+}
+
+.template-library-view .template-card-link-same {
+  color: #409eff;
+}
+
+.template-library-view .template-card-link-same:hover {
+  color: #66b1ff;
+}
+
+.template-library-view .template-card-dots {
+  color: #909399;
+  padding: 2px 4px;
+  margin-left: auto;
+}
+
+.template-library-view .template-card-dots:hover {
+  color: #606266;
+}
+
+.template-library-view .template-card-more {
+  margin-left: auto;
+}
+
+.template-meta {
+  display: flex;
+  gap: 8px;
+  font-size: 12px;
+  color: #999;
+}
+
+.template-style, .template-type {
+  background-color: #f5f5f5;
+  padding: 2px 8px;
+  border-radius: 12px;
+}
+
+.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 1 0;
+  min-width: 0;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  background-color: #e8e8e8;
+  overflow: hidden;
+}
+
+.canvas-wrapper {
+  position: relative;
+  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;
+  transition: box-shadow 0.2s;
+}
+
+.layer.selected {
+  box-shadow: 0 0 0 2px #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 {
+  flex: 0 0 320px;
+  width: 320px;
+  min-width: 320px;
+  background: #f5f6f8;
+  border-left: 1px solid #e8e8e8;
+  padding: 12px;
+  display: flex;
+  flex-direction: column;
+  box-sizing: border-box;
+  overflow: hidden;
+}
+
+.right-panel-menu {
+  display: flex;
+  gap: 0;
+  flex-shrink: 0;
+  margin-bottom: 12px;
+  background: #e8eaef;
+  border-radius: 8px;
+  padding: 3px;
+}
+.right-panel-menu-item {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 6px;
+  padding: 8px 12px;
+  font-size: 13px;
+  color: #606266;
+  background: transparent;
+  border: none;
+  border-radius: 6px;
+  cursor: pointer;
+  transition: background 0.2s, color 0.2s;
+}
+.right-panel-menu-item:hover {
+  color: #303133;
+}
+.right-panel-menu-item.active {
+  background: #fff;
+  color: #409eff;
+  font-weight: 600;
+  box-shadow: 0 1px 2px rgba(0,0,0,0.06);
+}
+
+.right-section {
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+  flex: 1 1 0;
+  overflow: hidden;
+}
+.right-section-props {
+  flex: 1 1 0;
+  min-height: 0;
+  overflow-y: auto;
+  padding-right: 4px;
+}
+.props-block {
+  margin-bottom: 16px;
+}
+.props-block:last-child {
+  margin-bottom: 0;
+}
+.props-block-title {
+  margin: 0 0 10px 0;
+  font-size: 13px;
+  font-weight: 600;
+  color: #303133;
+}
+.right-section-props .property-item {
+  margin-bottom: 8px;
+}
+.right-section-props .property-item:last-child {
+  margin-bottom: 0;
+}
+.right-section-props .layer-info-empty {
+  font-size: 12px;
+  color: #909399;
+  padding: 8px 0;
+}
+
+.layer-actions {
+  display: flex;
+  gap: 6px;
+  flex-shrink: 0;
+}
+
+.layer-list {
+  flex: 1;
+  margin-top: 4px;
+  min-height: 0;
+  overflow-y: auto;
+}
+
+/* 卡片式图层项:拖拽手柄 | 缩略图 | 名称 | 可见性 | 更多 */
+.layer-item-card {
+  display: flex;
+  align-items: center;
+  padding: 8px 10px;
+  margin-bottom: 6px;
+  background: #fff;
+  border-radius: 8px;
+  border: 1px solid #ebeef5;
+  cursor: pointer;
+  transition: all 0.2s;
+  gap: 8px;
+  user-select: none;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
+}
+
+.layer-item-card:hover {
+  border-color: #c0c4cc;
+  background: #fafafa;
+}
+
+.layer-item-card.selected {
+  background: #ecf5ff;
+  border-color: #409eff;
+  box-shadow: 0 0 0 1px #409eff;
+}
+
+.layer-item-card.dragging {
+  opacity: 0.6;
+  cursor: grabbing;
+}
+
+.layer-item-card.drag-over {
+  background: #d9ecff;
+  border: 2px dashed #409eff;
+  transform: scale(1.02);
+}
+
+.layer-drag-handle {
+  flex-shrink: 0;
+  font-size: 16px;
+  color: #909399;
+  cursor: grab;
+  transition: color 0.2s;
+}
+
+.layer-drag-handle:hover {
+  color: #409eff;
+}
+
+.layer-item-card.dragging .layer-drag-handle {
+  cursor: grabbing;
+}
+
+.layer-card-thumb {
+  flex-shrink: 0;
+  width: 40px;
+  height: 40px;
+  border-radius: 6px;
+  object-fit: cover;
+  border: 1px solid #ebeef5;
+  background: #f5f7fa;
+}
+
+.layer-card-thumb.text-thumb {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #fff;
+  color: #606266;
+  font-size: 14px;
+  font-weight: 600;
+}
+
+.layer-card-thumb.img-placeholder {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #c0c4cc;
+  font-size: 18px;
+}
+
+.layer-card-name {
+  flex: 1;
+  min-width: 0;
+  font-size: 13px;
+  color: #303133;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.layer-card-action {
+  flex-shrink: 0;
+  font-size: 16px;
+  color: #606266;
+  cursor: pointer;
+  padding: 2px;
+  transition: color 0.2s;
+}
+
+.layer-card-action:hover {
+  color: #409eff;
+}
+
+.layer-item-card.locked {
+  opacity: 0.85;
+}
+
+.layer-item-card.locked .layer-drag-handle {
+  cursor: not-allowed;
+  color: #c0c4cc;
+}
+
+.empty-tip {
+  text-align: center;
+  color: #999;
+  padding: 10px;
+  font-size: 11px;
+}
+
+.el-divider {
+  margin: 6px 0;
+}
+
+.save-template-btn {
+  width: 100%;
+  margin-top: 4px;
+  flex-shrink: 0;
+}
+
+/* 隐藏滚动条但保持功能 */
+.layer-list::-webkit-scrollbar,
+.layer-info::-webkit-scrollbar,
+.design-sections::-webkit-scrollbar {
+  width: 4px;
+}
+
+.layer-list::-webkit-scrollbar-thumb,
+.layer-info::-webkit-scrollbar-thumb,
+.design-sections::-webkit-scrollbar-thumb {
+  background-color: #ddd;
+  border-radius: 2px;
+}
+
+.layer-list::-webkit-scrollbar-track,
+.layer-info::-webkit-scrollbar-track,
+.design-sections::-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;
+  box-sizing: border-box !important;
+  height: 100vh !important;
+  max-height: 100vh !important;
+  overflow: hidden !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;
+  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;
+}
+
+/* 自定义上传样式,改成卡片风格 */
+.custom-upload {
+  width: 100%;
+  margin-bottom: 8px;
+}
+
+.custom-upload :deep(.el-upload-dragger) {
+  padding: 20px 16px;
+  background-color: #ffffff;
+  border: 1px dashed #dcdfe6;
+  border-radius: 4px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 12px;
+  box-sizing: border-box;
+}
+
+.custom-upload :deep(.upload-main) {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.custom-upload :deep(.upload-inner-button) {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  padding: 6px 16px;
+  border-radius: 999px;
+  border: 1px solid #dcdfe6;
+  background-color: #ffffff;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
+  cursor: pointer;
+  gap: 4px;
+}
+
+.custom-upload-icon {
+  font-size: 18px;
+  color: #409eff;
+}
+
+.custom-upload :deep(.upload-inner-text) {
+  font-size: 13px;
+  color: #333333;
+}
+
+.custom-upload :deep(.el-upload__text) {
+  margin: 0;
+  font-size: 14px;
+  color: #333333;
+}
+
+.custom-upload :deep(.el-upload__tip) {
+  font-size: 12px !important;
+  color: #888888 !important;
+  text-align: center;
+  line-height: 1.4;
+}
+</style>

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 113 - 854
src/view/TemplateManagement/TemplateDesign.vue


+ 8 - 0
src/view/TemplateManagement/WorkDetail.vue

@@ -0,0 +1,8 @@
+<template>
+</template>
+
+<script>
+</script>
+
+<style>
+</style>

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio