liuhairui 2 nedēļas atpakaļ
vecāks
revīzija
612a9d6bcd
1 mainītis faili ar 366 papildinājumiem un 57 dzēšanām
  1. 366 57
      src/view/TemplateManagement/TemplateDesign.vue

+ 366 - 57
src/view/TemplateManagement/TemplateDesign.vue

@@ -2,10 +2,9 @@
   <div class="template-design-container">
     <!-- 左侧工具栏 -->
     <div class="toolbar">
-      <!-- 可滚动区域:标签页 + 属性 -->
-      <div class="toolbar-scroll">
-        <div class="toolbar-tabs">
-          <el-tabs v-model="activeTab" type="card" @tab-click="handleTabClick">
+      <!-- 标签页 + 可滚动内容(保存按钮固定在底部) -->
+      <div class="toolbar-tabs">
+        <el-tabs v-model="activeTab" type="card" @tab-click="handleTabClick">
         <el-tab-pane label="模版设计" name="design">
           <el-upload
             class="custom-upload"
@@ -114,12 +113,36 @@
           </div>
           <div class="property-item">
             <span>字体:</span>
-            <el-select v-model="selectedLayer.fontFamily" size="small" style="flex: 1;">
+            <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="Verdana" value="Verdana" />
+              <el-option label="Tahoma" value="Tahoma" />
+              <el-option label="Georgia" value="Georgia" />
+              <el-option label="Courier New" value="Courier New" />
+              <el-option label="思源黑体(Source Han Sans SC)" value="Source Han Sans SC" />
+              <el-option label="思源黑体 CN(Source Han Sans CN)" value="Source Han Sans CN" />
+              <el-option label="思源宋体(Source Han Serif SC)" value="Source Han Serif SC" />
+              <el-option label="思源宋体 CN(Source Han Serif CN)" value="Source Han Serif CN" />
+              <el-option label="Noto Sans CJK SC" value="Noto Sans CJK SC" />
+              <el-option label="Noto Serif CJK SC" value="Noto Serif CJK SC" />
               <el-option label="宋体" value="SimSun" />
               <el-option label="黑体" value="SimHei" />
               <el-option label="微软雅黑" value="Microsoft YaHei" />
+              <el-option label="微软雅黑 UI" value="Microsoft YaHei UI" />
               <el-option label="楷体" value="KaiTi" />
+              <el-option label="仿宋" value="FangSong" />
+              <el-option label="华文中宋" value="STZhongsong" />
+              <el-option label="华文宋体" value="STSong" />
+              <el-option label="华文黑体" value="STHeiti" />
+              <el-option label="华文楷体" value="STKaiti" />
               <el-option label="Times New Roman" value="Times New Roman" />
             </el-select>
           </div>
@@ -165,46 +188,139 @@
           </div>
         </template>
           </div>
-          
-          <!-- 保存模版按钮 -->
-          <el-divider />
           </el-tab-pane>
           
-          <el-tab-pane label="素材选择" name="material" :label-class="'material-tab'">
-            
-            <div class="materials-list-full">
-              <el-skeleton v-if="materialsLoading" :rows="5" animated />
-              <div v-else-if="materials.length === 0" class="empty-materials">
-                暂无素材
+          <el-tab-pane label="素材库" name="material" :label-class="'material-tab'">
+            <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>
-              <div 
-                v-else 
-                class="material-item-full"
-                v-for="material in materials"
-                :key="material.id"
-                @click="addMaterialToCanvas(material)"
-              >
-                <img :src="material.material_url" :alt="material.id" />
-                <div class="material-overlay">
-                  <el-icon><Plus /></el-icon>
-                  <span>添加</span>
+
+              <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="material.material_url" :alt="material.id" />
+                    <div class="material-overlay">
+                      <el-icon><Plus /></el-icon>
+                      <span>添加</span>
+                    </div>
+                  </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="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="material.material_url" :alt="material.id" />
+                        
+                      </div>
+                    </div>
+                  </div>
+                </div>
+              </template>
             </div>
           </el-tab-pane>
         </el-tabs>
-        </div>
       </div>
 
-      <!-- 固定在底部的保存模版按钮 -->
       <el-divider class="save-template-divider" />
       <el-button
         type="primary"
-        :icon="Document"
         class="save-template-btn"
         @click="saveTemplate"
       >
-        保存模版
+        生成模版
       </el-button>
     </div>
     
@@ -335,7 +451,7 @@
 <script setup>
 import { ref, computed, reactive, onMounted, onUnmounted } from 'vue'
 import { ElMessage } from 'element-plus'
-import { Pointer, Rank, ArrowUp, ArrowDown, Delete, View, Hide, Lock, Unlock, Plus, Document, Picture, Refresh, Upload } from '@element-plus/icons-vue'
+import { Pointer, Rank, ArrowUp, ArrowDown, Delete, View, Hide, Lock, Unlock, Plus, Document, Picture, Refresh, Upload, Search, ArrowLeft } from '@element-plus/icons-vue'
 import { Material_List } from '@/api/mes/job'
 
 const canvasRef = ref(null)
@@ -442,7 +558,7 @@ const handleUpload = (options) => {
       
       layers.value.push(newLayer)
       selectedLayerId.value = newLayer.id
-      ElMessage.success('素材上传成功!')
+      // ElMessage.success('素材上传成功!')
     }
     img.src = e.target.result
   }
@@ -480,7 +596,7 @@ const addTextLayer = () => {
   layers.value.push(newLayer)
   selectedLayerId.value = newLayer.id
   textLayerCount.value++
-  ElMessage.success('文字图层已添加')
+  // ElMessage.success('文字图层已添加')
 }
 
 const textLayerCount = ref(0)
@@ -488,6 +604,54 @@ 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 () => {
@@ -495,7 +659,9 @@ const fetchMaterials = async () => {
   try {
     materialsLoading.value = true
     console.log('开始获取素材库数据')
-    const response = await Material_List()
+    const response = await Material_List({
+      search: (materialSearch.value || '').trim() || undefined
+    })
     console.log('素材库数据:', response)
     if (response.code === 0) {
       materials.value = response.data
@@ -513,6 +679,29 @@ const fetchMaterials = async () => {
   }
 }
 
+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)
@@ -553,7 +742,7 @@ const addMaterialToCanvas = (material) => {
     
     layers.value.push(newLayer)
     selectedLayerId.value = newLayer.id
-    ElMessage.success('素材已添加到画布!')
+    // ElMessage.success('素材已添加到画布!')
   }
   img.src = material.material_url
 }
@@ -642,8 +831,9 @@ const getLayerStyle = (layer) => {
   return {
     left: layer.x + 'px',
     top: layer.y + 'px',
-    width: layer.type === 'text' ? 'auto' : layer.width + 'px',
-    height: layer.type === 'text' ? 'auto' : layer.height + 'px',
+    // 文字和图片图层都使用显式宽高,方便通过属性和拖拽进行拉伸
+    width: layer.width + 'px',
+    height: layer.height + 'px',
     transform: `rotate(${layer.rotation}deg)`,
     opacity: layer.opacity / 100
   }
@@ -663,7 +853,7 @@ const getTextStyle = (layer) => {
     lineHeight: layer.lineHeight,
     letterSpacing: layer.letterSpacing + 'px',
     padding: '4px 8px',
-    whiteSpace: 'nowrap',
+    whiteSpace: 'pre-wrap',
     userSelect: 'none'
   }
 }
@@ -674,10 +864,10 @@ const editingTextLayer = ref(null)
 const handleLayerDblClick = (e, layer) => {
   if (layer.type === 'text' && !layer.locked) {
     editingTextLayer.value = layer
-    // 创建输入框进行编辑
-    const input = document.createElement('input')
-    input.value = layer.text
-    input.style.cssText = `
+    // 创建多行输入框进行编辑
+    const textarea = document.createElement('textarea')
+    textarea.value = layer.text
+    textarea.style.cssText = `
       position: absolute;
       left: ${layer.x}px;
       top: ${layer.y}px;
@@ -689,27 +879,25 @@ const handleLayerDblClick = (e, layer) => {
       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(input)
-    input.focus()
-    input.select()
+    canvas.appendChild(textarea)
+    textarea.focus()
+    textarea.select()
     
     const saveEdit = () => {
-      layer.text = input.value || '双击编辑文字'
-      canvas.removeChild(input)
+      layer.text = textarea.value || '双击编辑文字'
+      canvas.removeChild(textarea)
       editingTextLayer.value = null
     }
     
-    input.addEventListener('blur', saveEdit)
-    input.addEventListener('keydown', (e) => {
-      if (e.key === 'Enter') {
-        saveEdit()
-      }
-    })
+    textarea.addEventListener('blur', saveEdit)
   }
 }
 
@@ -873,7 +1061,7 @@ const deleteLayer = () => {
   if (index !== -1) {
     layers.value.splice(index, 1)
     selectedLayerId.value = layers.value.length > 0 ? layers.value[0].id : null
-    ElMessage.success('图层已删除')
+    // ElMessage.success('图层已删除')
   }
 }
 
@@ -1063,14 +1251,27 @@ const handleCanvasWheel = (e) => {
 .toolbar-tabs {
   display: flex;
   flex-direction: column;
+  flex: 1;
+  min-height: 0;
+}
+
+.toolbar-tabs :deep(.el-tabs) {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column !important;
 }
 
 .toolbar-tabs :deep(.el-tabs__header) {
+  order: 0;
   flex-shrink: 0;
   margin-bottom: 8px;
+  background: #fff;
+  z-index: 2;
 }
 
 .toolbar-tabs :deep(.el-tabs__content) {
+  order: 1;
   flex: 1;
   min-height: 0;
   overflow-y: auto;
@@ -1078,9 +1279,7 @@ const handleCanvasWheel = (e) => {
 }
 
 .toolbar-tabs :deep(.el-tab-pane) {
-  height: 100%;
-  display: flex;
-  flex-direction: column;
+  min-height: 0;
 }
 
 .toolbar-tabs :deep(.material-tab) {
@@ -1089,6 +1288,107 @@ const handleCanvasWheel = (e) => {
 }
 
 /* 完整素材库样式 */
+.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;
@@ -1124,6 +1424,15 @@ const handleCanvasWheel = (e) => {
   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;