2
0

2 Commits bf462ed207 ... 26329644ee

Autor SHA1 Nachricht Datum
  panqiuyao 26329644ee Merge remote-tracking branch 'origin/feature-frontend' into feature-frontend vor 2 Monaten
  panqiuyao a4bd51d86f feat(marketingEdit): 新增画布类型支持并优化编辑逻辑 vor 2 Monaten

+ 31 - 208
frontend/src/views/components/marketingEdit/index.vue

@@ -72,7 +72,7 @@ export default {
         color:"#fff",
         bg_color:'#fff',
         visible:false,
-        canvas_type: 'normal', // 画布类型:'normal'(普通画布), 'model'(模特图), 'scene'(场景图)
+        canvas_type: 'normal', // normal:普通画布 model:模特图 scene:场景图
         multi_goods_mode: '', // 多货号模式:''(默认单货号), 'single'(一个货号多角度), 'multiple'(多个货号同角度)
         max_goods_count: null, // 多货号模式下,最多可追加多少个货号
       }
@@ -84,73 +84,30 @@ export default {
     isEmpty(){
       return this.data.length === 0
     },
-    // 按 sort 字段排序后的画布数据
-    sortedData(){
-      return [...this.data].sort((a, b) => {
-        const sortA = a.sort !== undefined ? a.sort : (a._sortIndex !== undefined ? a._sortIndex : 999999)
-        const sortB = b.sort !== undefined ? b.sort : (b._sortIndex !== undefined ? b._sortIndex : 999999)
-        return sortA - sortB
-      })
-    },
-    // 根据排序后的数据获取当前画布
     this_canvas(){
-      const sorted = this.sortedData
-      return sorted[this.index] || this.data[this.index]
-    },
-    // 获取当前画布在原始数据中的索引
-    currentCanvasIndex(){
-      const sorted = this.sortedData
-      const currentCanvas = sorted[this.index]
-      if(!currentCanvas) return this.index
-      return this.data.findIndex(item => item === currentCanvas)
+      return this.data[this.index]
     }
   },
-  methods: {
-    // 辅助:通过排序后的索引,找到 data 中的真实索引
-    getDataIndexBySortedIndex(sortedIdx){
-      if(sortedIdx === undefined || sortedIdx === null) return -1
-      const sorted = this.sortedData
-      const item = sorted[sortedIdx]
-      if(!item) return -1
-      if(item._uid){
-        const found = this.data.findIndex(d => d._uid === item._uid)
-        if(found !== -1) return found
-      }
-      const foundByRef = this.data.findIndex(d => d === item)
-      return foundByRef !== -1 ? foundByRef : -1
-    },
-  },
   watch: {
     index(newValue, oldValue) {
       if (this.isEmpty) return
-      const sorted = this.sortedData
-      const targetCanvas = sorted[newValue]
-      // 场景图和模特图不能被激活,如果切换到这些类型,自动切换到下一个普通画布
-      if(targetCanvas && (targetCanvas.canvas_type === 'model' || targetCanvas.canvas_type === 'scene')){
-        // 查找下一个普通画布
-        let nextNormalIndex = -1
-        for(let i = newValue + 1; i < sorted.length; i++){
-          if(sorted[i].canvas_type === 'normal'){
-            nextNormalIndex = i
-            break
+      const target = this.data[newValue]
+      // 模特图/场景图不进入编辑,自动跳到下一个普通画布
+      if(target && (target.canvas_type === 'model' || target.canvas_type === 'scene')){
+        let nextIdx = -1
+        for(let i = newValue + 1; i < this.data.length; i++){
+          if(this.data[i].canvas_type !== 'model' && this.data[i].canvas_type !== 'scene'){
+            nextIdx = i; break
           }
         }
-        // 如果后面没有,往前找
-        if(nextNormalIndex === -1){
+        if(nextIdx === -1){
           for(let i = newValue - 1; i >= 0; i--){
-            if(sorted[i].canvas_type === 'normal'){
-              nextNormalIndex = i
-              break
+            if(this.data[i].canvas_type !== 'model' && this.data[i].canvas_type !== 'scene'){
+              nextIdx = i; break
             }
           }
         }
-        // 如果找到了普通画布,切换到它;否则保持原索引
-        if(nextNormalIndex !== -1){
-          this.$emit('update:index', nextNormalIndex)
-        } else {
-          // 如果没有普通画布,恢复到之前的索引
-          this.$emit('update:index', oldValue)
-        }
+        this.$emit('update:index', nextIdx === -1 ? oldValue : nextIdx)
         return
       }
       this.saveCanvasSnapshot(oldValue)
@@ -162,15 +119,6 @@ export default {
     }
   },
   mounted() {
-    // 初始化 sort 和唯一 _uid 字段(如果不存在)
-    this.data.forEach((item, idx) => {
-      if(item.sort === undefined){
-        item.sort = idx
-      }
-      if(!item._uid){
-        item._uid = `canvas-${Date.now()}-${Math.random().toString(16).slice(2)}-${idx}`
-      }
-    })
     if(this.$route?.name === 'editTpl'){
       this.loadTemplate()
     }
@@ -183,17 +131,11 @@ export default {
   methods: {
     loadTemplate(){
       if(this.hasLoadedTpl) return
-      const templateData = JSON.parse(JSON.stringify(canvas))
-      // 为每个画布初始化 sort 和唯一 _uid 字段
-      templateData.forEach((item, idx) => {
-        if(item.sort === undefined){
-          item.sort = idx
-        }
-        if(!item._uid){
-          item._uid = `canvas-${Date.now()}-${Math.random().toString(16).slice(2)}-${idx}`
-        }
-      })
-      this.data.splice(0, this.data.length, ...templateData)
+      const tplData = JSON.parse(JSON.stringify(canvas)).map(item => ({
+        canvas_type: item.canvas_type || 'normal',
+        ...item,
+      }))
+      this.data.splice(0, this.data.length, ...tplData)
       this.$emit('update:index', 0)
       this.hasLoadedTpl = true
     },
@@ -251,35 +193,12 @@ export default {
         return;
       }
 
-      // 模特图和场景图不需要初始化 canvas
+      // 模特图/场景图不初始化 fabric,直接跳过
       const canvasType = this.this_canvas?.canvas_type || 'normal'
       if (canvasType === 'model' || canvasType === 'scene') {
-        // 如果当前是场景图/模特图,自动切换到普通画布
-        const sorted = this.sortedData
-        let nextNormalIndex = -1
-        for(let i = 0; i < sorted.length; i++){
-          if(sorted[i].canvas_type === 'normal'){
-            nextNormalIndex = i
-            break
-          }
-        }
-        if(nextNormalIndex !== -1 && nextNormalIndex !== this.index){
-          this.$emit('update:index', nextNormalIndex)
-        }
         return
       }
 
-/*
-      //画布下不存在模板OSS地址
-      if(!this.this_canvas){
-        this.$emit('update:index', 0)
-        return;
-      }
-
-
-      if(this.this_canvas.tpl_url){
-        return;
-      }*/
       this.$emit('canvasStyle:update',{
         width: this.this_canvas.width,
         height: this.this_canvas.height
@@ -364,33 +283,16 @@ export default {
 
       if(this.canvasForm.type === 'add'){
         this.saveCanvasSnapshot()
-        // 计算新的 sort 值(取当前最大 sort + 1,或使用数组长度)
-        const maxSort = this.data.length > 0 
-          ? Math.max(...this.data.map(item => item.sort !== undefined ? item.sort : (item._sortIndex !== undefined ? item._sortIndex : 0))) 
-          : -1
-        // 若是模特图/场景图,名称默认改为对应名称
-        let canvasName = this.canvasForm.name
-        if(this.canvasForm.canvas_type === 'model'){
-          if(!canvasName || canvasName.startsWith('画布_') || canvasName === '场景图'){
-            canvasName = '模特图'
-          }
-        }else if(this.canvasForm.canvas_type === 'scene'){
-          if(!canvasName || canvasName.startsWith('画布_') || canvasName === '模特图'){
-            canvasName = '场景图'
-          }
-        }
         this.data.push({
                tpl_url:"",
                image_path:"",
-               name:canvasName,
+               name:this.canvasForm.name,
                width:FIXED_CANVAS_WIDTH,
                height:this.canvasForm.height,
                bg_color:this.canvasForm.bg_color,
                canvas_type: this.canvasForm.canvas_type || 'normal',
-               _uid: `canvas-${Date.now()}-${Math.random().toString(16).slice(2)}`,
                canvas_json:'',
                preview:'',
-               sort: maxSort + 1,
                multi_goods_mode: this.canvasForm.multi_goods_mode || '',
                max_goods_count: this.canvasForm.multi_goods_mode ? (this.canvasForm.max_goods_count || null) : null,
         })
@@ -475,50 +377,9 @@ export default {
     },
     handleSelectCanvas(index){
       if(index === this.index) return
-      // 场景图和模特图不能被选中/激活
-      const sorted = this.sortedData
-      const targetCanvas = sorted[index]
-      if(targetCanvas && (targetCanvas.canvas_type === 'model' || targetCanvas.canvas_type === 'scene')){
-        return
-      }
       this.saveCanvasSnapshot()
       this.$emit('update:index', index)
     },
-    handleMoveCanvas(currentIndex, direction){
-      // 先保存当前画布快照
-      this.saveCanvasSnapshot()
-      
-      const sorted = this.sortedData
-      if(currentIndex < 0 || currentIndex >= sorted.length) return
-      
-      let targetIndex
-      if(direction === 'up' && currentIndex > 0){
-        targetIndex = currentIndex - 1
-      } else if(direction === 'down' && currentIndex < sorted.length - 1){
-        targetIndex = currentIndex + 1
-      } else {
-        return // 无法移动
-      }
-      
-      // 获取要移动的画布和目标位置的画布
-      const movedCanvas = sorted[currentIndex]
-      const targetCanvas = sorted[targetIndex]
-      
-      // 交换 sort 值
-      const tempSort = movedCanvas.sort !== undefined ? movedCanvas.sort : currentIndex
-      const targetSort = targetCanvas.sort !== undefined ? targetCanvas.sort : targetIndex
-      
-      // 更新 sort 字段
-      movedCanvas.sort = targetSort
-      targetCanvas.sort = tempSort
-      
-      // 如果当前激活的是被移动的画布,更新索引
-      if(this.index === currentIndex){
-        this.$emit('update:index', targetIndex)
-      } else if(this.index === targetIndex){
-        this.$emit('update:index', currentIndex)
-      }
-    },
     handleDeleteCanvas(targetIndex){
       if(this.data.length <= 1){
         this.$message.warning('至少需要保留一个画布')
@@ -582,7 +443,7 @@ export default {
               // 计算目标元素相对于滚动容器的位置
               const containerRect = scrollContainer.getBoundingClientRect()
               const targetRect = target.getBoundingClientRect()
-              
+
               // 计算需要滚动的距离(目标元素顶部 - 容器顶部 - 导航栏高度100px)
               const scrollTop = scrollContainer.scrollTop
               const targetPosition = scrollTop + (targetRect.top - containerRect.top) - 100
@@ -607,15 +468,13 @@ export default {
     },
     canvasBodyStyle(item){
       const canvasType = item?.canvas_type || 'normal'
-      // 模特图和场景图显示为正方形
+      // 模特图 / 场景图:保持方形显示
       if(canvasType === 'model' || canvasType === 'scene'){
         return {
           width: `${FIXED_CANVAS_WIDTH}px`,
           height: `${FIXED_CANVAS_WIDTH}px`,
-          margin: '0 auto',
-          display: 'flex',
-          alignItems: 'center',
-          justifyContent: 'center'
+          lineHeight: `${FIXED_CANVAS_WIDTH}px`,
+          margin: '0 auto'
         }
       }
       // 普通画布
@@ -653,11 +512,9 @@ export default {
           width: item.width,
           height: item.height,
           bg_color: item.bg_color,
-          canvas_type: item.canvas_type || 'normal',
           name: item.name || '',
           tpl_url: item.tpl_url || '',
           image_path: item.image_path || '',
-          sort: item.sort !== undefined ? item.sort : idx,
           multi_goods_mode: item.multi_goods_mode || '',
           max_goods_count: item.max_goods_count || null,
         }
@@ -698,11 +555,9 @@ export default {
        return { bundles }
     },
     saveCanvasSnapshot(targetIndex){
-      // targetIndex 为排序后的索引,需要映射到 data 的真实索引
-      const sortedIdx = typeof targetIndex === 'number' ? targetIndex : this.index
-      const dataIndex = this.getDataIndexBySortedIndex(sortedIdx)
-      if(!this.fcanvas || dataIndex === undefined || dataIndex === null || dataIndex < 0) return
-      const canvasData = this.data[dataIndex]
+      const snapshotIndex = typeof targetIndex === 'number' ? targetIndex : this.index
+      if(!this.fcanvas || snapshotIndex === undefined || snapshotIndex === null) return
+      const canvasData = this.data[snapshotIndex]
       if(!canvasData) return
       const json = JSON.stringify(this.fcanvas.toJSON(['name','sort','mtr','id','selectable','erasable','data-key','data-type','data-value']))
       let preview = canvasData.preview || ''
@@ -942,42 +797,6 @@ export default {
   border-bottom: 1px solid #f0f0f0;
 }
 
-.canvas-special-placeholder {
-  width: 100%;
-  height: 100%;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  background: #f5f5f5;
-  border: 2px dashed #d0d0d0;
-  border-radius: 4px;
-  
-  &.model {
-    border-color: #68BCA5;
-    background: rgba(104, 188, 165, 0.05);
-  }
-  
-  &.scene {
-    border-color: #409EFF;
-    background: rgba(64, 158, 255, 0.05);
-  }
-}
-
-.canvas-special-placeholder .special-placeholder-content {
-  font-size: 24px;
-  font-weight: bold;
-  color: #666;
-  text-align: center;
-}
-
-.canvas-special-placeholder.model .special-placeholder-content {
-  color: #68BCA5;
-}
-
-.canvas-special-placeholder.scene .special-placeholder-content {
-  color: #409EFF;
-}
-
 .fixed-width-tip {
   line-height: 32px;
   color: #666;
@@ -985,3 +804,7 @@ export default {
 
 
 </style>
+
+
+
+

+ 51 - 72
frontend/src/views/components/marketingEdit/tpl/header.js

@@ -19,52 +19,28 @@ export  default function tpl(){
                       <template #dropdown>
                         <el-dropdown-menu>
                           <el-dropdown-item
-                            v-for="(item,idx) in sortedData"
-                            :key="item._uid || item.id || (item.tpl_url || '') + '-' + idx"
+                            v-for="(item,idx) in data"
+                            :key="(item.id || item.tpl_url || idx) + '-' + idx"
                             @click.native.stop
                           >
                             <div style="display: flex; align-items: center; justify-content: space-between; width: 100%;">
                               <span 
-                                :class="(idx === index && item.canvas_type !== 'model' && item.canvas_type !== 'scene') ? 'c-primary' : ''" 
-                                :style="item.canvas_type === 'model' || item.canvas_type === 'scene' ? 'flex: 1; cursor: not-allowed; color: #999;' : 'flex: 1; cursor: pointer;'"
+                                :class="idx === index ? 'c-primary' : ''" 
+                                style="flex: 1; cursor: pointer;"
                                 @click="handleSelectCanvas(idx)"
                               >
                                 {{item.name || ('画布 ' + (idx + 1))}}
-                                <span v-if="item.canvas_type === 'model'" style="color: #68BCA5; margin-left: 4px;">(模特图)</span>
-                                <span v-if="item.canvas_type === 'scene'" style="color: #409EFF; margin-left: 4px;">(场景图)</span>
                               </span>
-                              <div style="display: flex; align-items: center; gap: 4px;">
-                                <el-button
-                                  text
-                                  size="small"
-                                  :disabled="idx === 0"
-                                  @click="handleMoveCanvas(idx, 'up')"
-                                  style="padding: 0 4px;"
-                                  title="上移"
-                                >
-                                  ↑
-                                </el-button>
-                                <el-button
-                                  text
-                                  size="small"
-                                  :disabled="idx === sortedData.length - 1"
-                                  @click="handleMoveCanvas(idx, 'down')"
-                                  style="padding: 0 4px;"
-                                  title="下移"
-                                >
-                                  ↓
-                                </el-button>
-                                <el-button
-                                  type="danger"
-                                  text
-                                  size="small"
-                                  :disabled="data.length === 1"
-                                  @click="handleDeleteCanvas(idx)"
-                                  style="margin-left: 4px; padding: 0 8px;"
-                                >
-                                  删除
-                                </el-button>
-                              </div>
+                              <el-button
+                                type="danger"
+                                text
+                                size="small"
+                                :disabled="data.length === 1"
+                                @click="handleDeleteCanvas(idx)"
+                                style="margin-left: 10px; padding: 0 8px;"
+                              >
+                                删除
+                              </el-button>
                             </div>
                           </el-dropdown-item>
                         </el-dropdown-menu>
@@ -101,40 +77,43 @@ export  default function tpl(){
           <!--      <el-form-item label="宽度">
                   <div class="fixed-width-tip">800(固定)</div>
                 </el-form-item>-->
-                <el-form-item v-if="canvasForm.canvas_type === 'normal'" label="高度">
-                  <el-input v-model.number="canvasForm.height" placeholder="请输入高度"></el-input>
-                </el-form-item>
-                <el-form-item v-if="canvasForm.canvas_type === 'normal'" label="背景颜色">
-                    <el-color-picker v-model="canvasForm.bg_color" />
-                </el-form-item>
-                <el-form-item v-if="canvasForm.canvas_type === 'normal'" label="多货号模式">
-                  <el-radio-group v-model="canvasForm.multi_goods_mode">
-                    <el-radio label="">默认(单货号)</el-radio>
-                    <el-radio label="single">一个货号多角度</el-radio>
-                    <el-radio label="multiple">多个货号同角度</el-radio>
-                  </el-radio-group>
-                  <div style="font-size: 12px; color: #999; margin-top: 8px; line-height: 1.6;">
-                    <div v-if="canvasForm.multi_goods_mode === ''">默认模式,画布对应单个货号</div>
-                    <div v-else-if="canvasForm.multi_goods_mode === 'single'">一个画布对应一个货号,画布中包含该货号不同角度的图片</div>
-                    <div v-else-if="canvasForm.multi_goods_mode === 'multiple'">画布中含有多个货号,画布中包含多个货号同一角度的图片</div>
-                  </div>
-                </el-form-item>
-                <el-form-item
-                  v-if="canvasForm.canvas_type === 'normal' && (canvasForm.multi_goods_mode === 'single' || canvasForm.multi_goods_mode === 'multiple')"
-                  label="最多货号数量"
-                >
-                  <el-input-number
-                    v-model="canvasForm.max_goods_count"
-                    :min="1"
-                    :max="99"
-                    :step="1"
-                    controls-position="right"
-                    placeholder="请输入最多可追加的货号数量"
-                  />
-                  <div style="font-size: 12px; color: #999; margin-top: 4px;">
-                    用于限制此画布在生成详情图时,最多会为多少个货号生成内容;为空则不限制。
-                  </div>
-                </el-form-item>
+                <template v-if="canvasForm.canvas_type === 'normal'">
+                
+                  <el-form-item label="高度">
+                    <el-input v-model.number="canvasForm.height" placeholder="请输入高度"></el-input>
+                  </el-form-item>
+                  <el-form-item label="背景颜色">
+                      <el-color-picker v-model="canvasForm.bg_color" />
+                  </el-form-item>
+                  <el-form-item label="多货号模式">
+                    <el-radio-group v-model="canvasForm.multi_goods_mode">
+                      <el-radio label="">默认(单货号)</el-radio>
+                      <el-radio label="single">一个货号多角度</el-radio>
+                      <el-radio label="multiple">多个货号同角度</el-radio>
+                    </el-radio-group>
+                    <div style="font-size: 12px; color: #999; margin-top: 8px; line-height: 1.6;">
+                      <div v-if="canvasForm.multi_goods_mode === ''">默认模式,画布对应单个货号</div>
+                      <div v-else-if="canvasForm.multi_goods_mode === 'single'">一个画布对应一个货号,画布中包含该货号不同角度的图片</div>
+                      <div v-else-if="canvasForm.multi_goods_mode === 'multiple'">画布中含有多个货号,画布中包含多个货号同一角度的图片</div>
+                    </div>
+                  </el-form-item>
+                  <el-form-item
+                    v-if="canvasForm.multi_goods_mode === 'single' || canvasForm.multi_goods_mode === 'multiple'"
+                    label="最多货号数量"
+                  >
+                    <el-input-number
+                      v-model="canvasForm.max_goods_count"
+                      :min="1"
+                      :max="99"
+                      :step="1"
+                      controls-position="right"
+                      placeholder="请输入最多可追加的货号数量"
+                    />
+                    <div style="font-size: 12px; color: #999; margin-top: 4px;">
+                      用于限制此画布在生成详情图时,最多会为多少个货号生成内容;为空则不限制。
+                    </div>
+                  </el-form-item>
+                </template>
               </el-form>
               <div slot="footer" class="dialog-footer flex right">
                 <el-button @click="canvasForm.visible = false">取 消</el-button>

+ 5 - 10
frontend/src/views/components/marketingEdit/tpl/view.js

@@ -8,14 +8,14 @@ export default function() {
       <div v-if="!isEmpty" class="canvas-stack">
         <div
           class="canvas-stack_item"
-          v-for="(item, idx) in sortedData"
-          :key="item._uid || item.id || (item.tpl_url || '') + '-' + idx"
+          v-for="(item, idx) in data"
+          :key="(item.id || item.tpl_url || idx) + '-' + idx"
           :ref="'canvasItem-' + idx"
           :style="{backgroundColor: item.bg_color || '#fff'}"
         >
           <div
             class="canvas-stack_body"
-            :class="(idx === index && item.canvas_type !== 'model' && item.canvas_type !== 'scene') ? 'active' : 'inactive'"
+            :class="idx === index ? 'active' : 'inactive'"
             :style="canvasBodyStyle(item)"
           >
             <el-button
@@ -39,16 +39,11 @@ export default function() {
             </template>
             <!-- 普通画布 -->
             <template v-else>
-              <div 
-                v-if="idx !== index"  
-                class="canvas-stack_placeholder"
-                @click="handleSelectCanvas(idx)"
-                style="cursor: pointer;"
-              >
+              <div v-if="idx !== index"  class="canvas-stack_placeholder" @click="handleSelectCanvas(idx)" style="cursor: pointer;">
                 <img v-if="item.preview" :src="item.preview" alt="">
                 <img v-else-if="item.image_path" :src="item.image_path" alt="">
                 <img v-else-if="item.tpl_url" :src="item.tpl_url" alt="">
-                <span v-else>点击"切换到此画布"开始编辑</span>
+                <span v-else>点击“切换到此画布”开始编辑</span>
               </div>
               <template  v-else >
                 <canvas