فهرست منبع

重构(摄影):更新样式并改进图像渲染逻辑

- 调整了 detail.vue 中的布局样式,以实现更好的对齐和间距。
- 修改了 detail.vue 中的图像高度,以提升视觉一致性。
- 增强了 generateImagesRender.js 中的图像渲染逻辑,包括图像压缩和动态 URL 处理。
- 更新了 goods4.json 以反映新的产品数据,并新增了 goods5.json 以提供额外的产品信息。
- 改进了 index.vue,以集成新的产品数据,并调整图标大小以提升 UI 一致性。
kongwenhao 2 روز پیش
والد
کامیت
4b2f374a0f

+ 5 - 0
debug.log

@@ -0,0 +1,5 @@
+[1222/163953.602:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: 系统找不到指定的文件。 (0x2)
+[1222/163955.906:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: 系统找不到指定的文件。 (0x2)
+[1222/163959.070:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: 系统找不到指定的文件。 (0x2)
+[1222/164001.295:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: 系统找不到指定的文件。 (0x2)
+[1222/164222.470:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: 系统找不到指定的文件。 (0x2)

+ 3 - 4
frontend/src/views/Photography/detail.vue

@@ -2510,7 +2510,7 @@ const selectFolder = () => {
 
 
     .publish-form-item {
     .publish-form-item {
       display: flex;
       display: flex;
-      flex-direction: column;
+      align-items: center;
       gap: 8px;
       gap: 8px;
 
 
       .publish-label {
       .publish-label {
@@ -2521,8 +2521,7 @@ const selectFolder = () => {
       }
       }
 
 
       .publish-select {
       .publish-select {
-        width: 100%;
-
+        flex-grow: 1;
         ::v-deep {
         ::v-deep {
           .el-input.is-disabled .el-input__inner {
           .el-input.is-disabled .el-input__inner {
             background-color: #F5F6F7;
             background-color: #F5F6F7;
@@ -2771,7 +2770,7 @@ const selectFolder = () => {
     cursor: pointer;
     cursor: pointer;
     background: #f0f0f0;
     background: #f0f0f0;
     position: relative;
     position: relative;
-    height: 660px;
+    height: 594px;
     overflow: hidden;
     overflow: hidden;
 
 
     &.active {
     &.active {

+ 446 - 178
frontend/src/views/components/marketingEdit/generateImagesRender.js

@@ -47,7 +47,6 @@ import { buildRenderPlans, normalizeGoods } from './generateImagesPlan'
 //           ? JSON.parse(canvasItem.canvas_json)
 //           ? JSON.parse(canvasItem.canvas_json)
 //           : JSON.parse(JSON.stringify(canvasItem.canvas_json))
 //           : JSON.parse(JSON.stringify(canvasItem.canvas_json))
 //       } catch (e) {
 //       } catch (e) {
-//         console.warn('[generateImagesRender] parse canvas_json failed', e)
 //         return resolve(null)
 //         return resolve(null)
 //       }
 //       }
 
 
@@ -68,15 +67,14 @@ import { buildRenderPlans, normalizeGoods } from './generateImagesPlan'
 //         const imgPlaceholders = objs.filter((o) => o && o['data-type'] === 'img')
 //         const imgPlaceholders = objs.filter((o) => o && o['data-type'] === 'img')
 
 
 //         if (mode === 'multiple') {
 //         if (mode === 'multiple') {
-//           // 多个货号同角度:同一画布中,有多个 img 占位,按顺序对应 skuIndexes
 //           imgPlaceholders.forEach((obj, idx) => {
 //           imgPlaceholders.forEach((obj, idx) => {
 //             const slotIndex = idx % perCanvasSlots
 //             const slotIndex = idx % perCanvasSlots
 //             const sku = usedSkus[slotIndex]
 //             const sku = usedSkus[slotIndex]
+//             const angleKey = obj['data-key']
 //             if (!sku) {
 //             if (!sku) {
 //               obj.visible = false
 //               obj.visible = false
 //               return
 //               return
 //             }
 //             }
-//             const angleKey = obj['data-key']
 //             const url = (sku.pics && sku.pics[angleKey]) || ''
 //             const url = (sku.pics && sku.pics[angleKey]) || ''
 //             if (!url) {
 //             if (!url) {
 //               obj.visible = false
 //               obj.visible = false
@@ -190,9 +188,25 @@ import { buildRenderPlans, normalizeGoods } from './generateImagesPlan'
 //         fcanvas.loadFromJSON(json, () => {
 //         fcanvas.loadFromJSON(json, () => {
 //           try {
 //           try {
 //             fcanvas.renderAll()
 //             fcanvas.renderAll()
+
+//             // 获取商品图宽高,并在导出时根据宽度做压缩控制
+//             const MAX_WIDTH = 2048
+//             // 默认导出倍数(原来为 2,保持清晰度)
+//             let exportMultiplier = 2
+
+//             // 计算按当前导出倍数后的宽度
+//             const expectedWidth = width * exportMultiplier
+//             if (expectedWidth > MAX_WIDTH) {
+//               // 根据最大宽度反推需要的缩放倍数(<= 1)
+//               exportMultiplier = MAX_WIDTH / width
+//             }
+
+//             const finalWidth = Math.round(width * exportMultiplier)
+//             const finalHeight = Math.round(height * exportMultiplier)
+
 //             const dataUrl = fcanvas.toDataURL({
 //             const dataUrl = fcanvas.toDataURL({
 //               format: 'jpeg',
 //               format: 'jpeg',
-//               multiplier:2,
+//               multiplier: exportMultiplier,
 //               enableRetinaScaling: true,
 //               enableRetinaScaling: true,
 //             })
 //             })
 
 
@@ -201,9 +215,10 @@ import { buildRenderPlans, normalizeGoods } from './generateImagesPlan'
 //             resolve({
 //             resolve({
 //               canvasIndex,
 //               canvasIndex,
 //               dataUrl,
 //               dataUrl,
+//               width: finalWidth,
+//               height: finalHeight,
 //             })
 //             })
 //           } catch (e) {
 //           } catch (e) {
-//             console.warn('[generateImagesRender] render one failed in callback', e)
 //             try {
 //             try {
 //               fcanvas.dispose()
 //               fcanvas.dispose()
 //             } catch (e2) {}
 //             } catch (e2) {}
@@ -211,7 +226,6 @@ import { buildRenderPlans, normalizeGoods } from './generateImagesPlan'
 //           }
 //           }
 //         })
 //         })
 //       } catch (e) {
 //       } catch (e) {
-//         console.warn('[generateImagesRender] render one failed', e)
 //         resolve(null)
 //         resolve(null)
 //       }
 //       }
 //     })
 //     })
@@ -229,164 +243,327 @@ import { buildRenderPlans, normalizeGoods } from './generateImagesPlan'
 
 
 //   return results
 //   return results
 // }
 // }
+
 export async function renderImagesByPlans(plans, canvasList, skus) {
 export async function renderImagesByPlans(plans, canvasList, skus) {
-  console.log('=== 开始生成图片 ===')
-  console.log('skus 数据:', skus)
-  console.log('canvasList:', canvasList)
-  console.log('plans:', plans)
-  
   const results = []
   const results = []
 
 
-  const renderOne = (canvasItem, planForCanvas, imagePlan) =>
-    new Promise((resolve) => {
+  // 工具函数:只检查并压缩图片,返回压缩后的图片URL
+  const compressImageIfNeeded = (url, maxWidth = 2048) => {
+    return new Promise((resolve) => {
+      
+      if (!url) {
+        resolve(null)
+        return
+      }
+
+      const img = new Image()
+      img.crossOrigin = 'anonymous'
+      
+      img.onload = () => {
+        
+        // 检查图片是否需要压缩
+        if (img.width <= maxWidth) {
+          resolve(url)
+          return
+        }
+        // 计算压缩后的尺寸(等比例压缩)
+        const scale = maxWidth / img.width
+        const newWidth = maxWidth
+        const newHeight = Math.round(img.height * scale)
+
+        // 创建canvas进行压缩
+        const canvas = document.createElement('canvas')
+        canvas.width = newWidth
+        canvas.height = newHeight
+        
+        const ctx = canvas.getContext('2d')
+        if (!ctx) {
+          console.error('无法获取canvas 2d上下文')
+          resolve(url)
+          return
+        }
+        
+        // 重要:设置canvas背景为透明
+        // 方法1:清除画布,设置为透明
+        // ctx.clearRect(0, 0, newWidth, newHeight)
+        
+        // 方法2:或者使用透明矩形填充
+        ctx.fillStyle = 'rgba(0, 0, 0, 0)'
+        ctx.fillRect(0, 0, newWidth, newHeight)
+        
+        // 设置高质量压缩参数
+        ctx.imageSmoothingEnabled = true
+        ctx.imageSmoothingQuality = 'high'
+        
+        // 绘制压缩后的图片
+        ctx.drawImage(img, 0, 0, newWidth, newHeight)
+        
+        // 返回压缩后的dataURL,使用PNG格式保持透明通道(如果原图有透明背景)
+        // 但如果是JPG商品图,通常没有透明通道,也可以用PNG
+        const dataUrl = canvas.toDataURL('image/png')  // 使用PNG确保无黑色背景
+        
+     
+        
+        
+        canvas.remove()
+        resolve(dataUrl)
+      }
+      img.onerror = (error) => {
+        resolve(url) // 返回原始URL作为fallback
+      }
+      
+      img.src = url
+    })
+  }
+
+  // 工具:从 canvas_json 创建一个离屏 fabric.Canvas,并应用一次性的渲染逻辑
+  const renderOne = async (canvasItem, planForCanvas, imagePlan) => {
+
+    
+    return new Promise(async (resolve) => {
       if (!canvasItem) {
       if (!canvasItem) {
-        console.warn('canvasItem 为空')
         return resolve(null)
         return resolve(null)
       }
       }
 
 
       const { canvasIndex } = planForCanvas
       const { canvasIndex } = planForCanvas
       const { skuIndexes } = imagePlan
       const { skuIndexes } = imagePlan
       
       
-      console.log(`\n=== 渲染画布 ${canvasIndex} ===`)
-      console.log('canvasItem:', canvasItem)
-      console.log('skuIndexes:', skuIndexes)
-      console.log('usedSkus:', skuIndexes.map(idx => idx != null ? skus[idx]?.sku : null))
 
 
-      // 模特/场景占位处理
+      // 模特/场景占位:直接返回类型,不渲染
       if (canvasItem.canvas_json === 'model') {
       if (canvasItem.canvas_json === 'model') {
-        console.log('跳过模特占位')
         return resolve({
         return resolve({
           canvasIndex,
           canvasIndex,
           dataUrl: 'model',
           dataUrl: 'model',
         })
         })
       }
       }
       if (canvasItem.canvas_json === 'scene') {
       if (canvasItem.canvas_json === 'scene') {
-        console.log('跳过场景占位')
         return resolve({
         return resolve({
           canvasIndex,
           canvasIndex,
           dataUrl: 'scene',
           dataUrl: 'scene',
         })
         })
       }
       }
 
 
-      // 解析 JSON
+      // 解析 JSON 为可修改的对象
       let json
       let json
       try {
       try {
         json = typeof canvasItem.canvas_json === 'string'
         json = typeof canvasItem.canvas_json === 'string'
           ? JSON.parse(canvasItem.canvas_json)
           ? JSON.parse(canvasItem.canvas_json)
           : JSON.parse(JSON.stringify(canvasItem.canvas_json))
           : JSON.parse(JSON.stringify(canvasItem.canvas_json))
-        console.log('解析 canvas_json 成功')
       } catch (e) {
       } catch (e) {
-        console.error('解析 canvas_json 失败:', e)
-        console.log('原始 canvas_json:', canvasItem.canvas_json)
         return resolve(null)
         return resolve(null)
       }
       }
 
 
       const width = Number(canvasItem.width) || 395
       const width = Number(canvasItem.width) || 395
       const height = Number(canvasItem.height) || 600
       const height = Number(canvasItem.height) || 600
       const bgColor = canvasItem.bg_color || '#fff'
       const bgColor = canvasItem.bg_color || '#fff'
+      
 
 
       try {
       try {
         const mode = canvasItem.multi_goods_mode || ''
         const mode = canvasItem.multi_goods_mode || ''
         const perCanvasSlots = planForCanvas.perCanvasSlots || 1
         const perCanvasSlots = planForCanvas.perCanvasSlots || 1
-        const usedSkus = (skuIndexes || []).map((idx) => {
-          const sku = idx != null ? skus[idx] : null
-          if (sku) {
-            console.log(`货号 ${sku.sku} 的图片:`, sku.pics)
-          }
-          return sku
-        })
+        const usedSkus = (skuIndexes || []).map((idx) => (idx != null ? skus[idx] : null))
+        
 
 
-        console.log('mode:', mode)
-        console.log('perCanvasSlots:', perCanvasSlots)
-        console.log('实际使用的 sku:', usedSkus.map(s => s?.sku))
 
 
-        // 处理图片占位
+        // 获取画布对象
         const objs = (json && Array.isArray(json.objects)) ? json.objects : []
         const objs = (json && Array.isArray(json.objects)) ? json.objects : []
-        console.log('总对象数:', objs.length)
-        
+
+        // 1) 处理图片占位(data-type = img)
         const imgPlaceholders = objs.filter((o) => o && o['data-type'] === 'img')
         const imgPlaceholders = objs.filter((o) => o && o['data-type'] === 'img')
-        console.log('图片占位符数量:', imgPlaceholders.length)
         
         
-        const textPlaceholders = objs.filter((o) => o && o['data-type'] === 'text')
-        console.log('文字占位符数量:', textPlaceholders.length)
-
-        // 详细检查每个图片占位符
-        imgPlaceholders.forEach((obj, idx) => {
-          console.log(`图片占位符 ${idx}:`, {
-            dataKey: obj['data-key'],
-            visible: obj.visible,
-            src: obj.src,
-            dataValue: obj['data-value']
-          })
-        })
 
 
+
+        // 为每个图片占位生成压缩后的URL
+        const imageUrlMap = new Map()
+        
+        // 收集所有需要压缩的图片URL
+        const urlsToCompress = []
+        
         if (mode === 'multiple') {
         if (mode === 'multiple') {
-          console.log('使用 multiple 模式')
           imgPlaceholders.forEach((obj, idx) => {
           imgPlaceholders.forEach((obj, idx) => {
             const slotIndex = idx % perCanvasSlots
             const slotIndex = idx % perCanvasSlots
             const sku = usedSkus[slotIndex]
             const sku = usedSkus[slotIndex]
-            console.log(`占位符 ${idx} -> 货位 ${slotIndex} -> 货号:`, sku?.sku)
+            const angleKey = obj['data-key']
             
             
             if (!sku) {
             if (!sku) {
-              console.log(`货位 ${slotIndex} 无货号,隐藏图层`)
-              obj.visible = false
               return
               return
             }
             }
             
             
-            const angleKey = obj['data-key']
             const url = (sku.pics && sku.pics[angleKey]) || ''
             const url = (sku.pics && sku.pics[angleKey]) || ''
-            console.log(`角度 ${angleKey} 的图片URL:`, url)
-            
             if (!url) {
             if (!url) {
-              console.log(`货号 ${sku.sku} 无角度 ${angleKey} 图片,隐藏图层`)
-              obj.visible = false
               return
               return
             }
             }
             
             
-            console.log(`设置图片 ${idx}: ${angleKey} = ${url}`)
-            obj.visible = true
-            obj['data-value'] = url
-            obj.src = url
-            
-            // 在 Electron 中,需要设置 crossOrigin
-            if (window.electron) {
-              obj.crossOrigin = 'anonymous'
-            }
+            urlsToCompress.push({ url, objId: `${idx}_${angleKey}`, originalObj: obj, angleKey, skuIndex: slotIndex })
           })
           })
         } else {
         } else {
-          console.log('使用 single/默认模式')
+          // 默认 / single:一个货号多角度,一张图只用一个货号
           const sku = usedSkus[0]
           const sku = usedSkus[0]
-          console.log('使用的货号:', sku?.sku)
+          if (!sku) {
+          }
           
           
           imgPlaceholders.forEach((obj, idx) => {
           imgPlaceholders.forEach((obj, idx) => {
+            if (!sku) return
+            
+            const angleKey = obj['data-key']
+            const url = (sku.pics && sku.pics[angleKey]) || ''
+            if (!url) {
+              return
+            }
+            
+            urlsToCompress.push({ url, objId: `${idx}_${angleKey}`, originalObj: obj, angleKey })
+          })
+        }
+
+        
+        // 并发压缩所有图片
+        const compressResults = await Promise.all(
+          urlsToCompress.map(async ({ url, objId, originalObj, angleKey }) => {
+            try {
+              const compressedUrl = await compressImageIfNeeded(url , originalObj.width)
+              return { objId, compressedUrl, originalObj, angleKey }
+            } catch (e) {
+              return { objId, compressedUrl: null, originalObj, angleKey }
+            }
+          })
+        )
+
+        // 更新imageUrlMap
+        compressResults.forEach(({ objId, compressedUrl }) => {
+          if (compressedUrl) {
+            imageUrlMap.set(objId, compressedUrl)
+          }
+        })
+
+
+        // 更新JSON中的图片URL
+        if (mode === 'multiple') {
+          imgPlaceholders.forEach((obj, idx) => {
+            const slotIndex = idx % perCanvasSlots
+            const sku = usedSkus[slotIndex]
+            const angleKey = obj['data-key']
+            
             if (!sku) {
             if (!sku) {
-              console.log('无货号,隐藏所有图层')
               obj.visible = false
               obj.visible = false
               return
               return
             }
             }
             
             
-            const angleKey = obj['data-key']
-            const url = (sku.pics && sku.pics[angleKey]) || ''
-            console.log(`图片 ${idx} 角度 ${angleKey}:`, url)
+            const objId = `${idx}_${angleKey}`
+            const compressedUrl = imageUrlMap.get(objId)
             
             
-            if (!url) {
-              console.log(`无 ${angleKey} 图片,隐藏图层`)
+            if (compressedUrl) {
+              obj.visible = true
+              obj['data-value'] = compressedUrl
+              obj.src = compressedUrl
+            } else {
+              const url = (sku.pics && sku.pics[angleKey]) || ''
+              if (!url) {
+                obj.visible = false
+                return
+              }
+              obj.visible = true
+              obj['data-value'] = url
+              obj.src = url
+            }
+          })
+        } else {
+          // 默认 / single:一个货号多角度,一张图只用一个货号
+          const sku = usedSkus[0]
+          imgPlaceholders.forEach((obj, idx) => {
+            if (!sku) {
               obj.visible = false
               obj.visible = false
               return
               return
             }
             }
             
             
-            console.log(`设置图片 ${idx}: ${angleKey} = ${url}`)
-            obj.visible = true
-            obj['data-value'] = url
-            obj.src = url
+            const angleKey = obj['data-key']
+            const objId = `${idx}_${angleKey}`
+            const compressedUrl = imageUrlMap.get(objId)
             
             
-            if (window.electron) {
-              obj.crossOrigin = 'anonymous'
+            if (compressedUrl) {
+              obj.visible = true
+              obj['data-value'] = compressedUrl
+              obj.src = compressedUrl
+            } else {
+              const url = (sku.pics && sku.pics[angleKey]) || ''
+              if (!url) {
+                obj.visible = false
+                return
+              }
+              obj.visible = true
+              obj['data-value'] = url
+              obj.src = url
             }
             }
           })
           })
         }
         }
 
 
-        // 创建 canvas
+        // 2) 处理文字占位(data-type = text)
+        const textPlaceholders = objs.filter((o) => o && o['data-type'] === 'text')
+
+        if (textPlaceholders.length) {
+          // 通用的 key -> 文本 映射函数,
+          const mapKeyToText = (sku, key, defaultVal) => {
+            if (!sku) return defaultVal
+            let textVal = defaultVal || ''
+            if (key === '颜色') {
+              textVal = sku.color || textVal
+            } else if (key === '货号') {
+              textVal = sku.sku || textVal
+            }
+            // 兜底:去 raw 里找同名字段(支持 卖点 / 使用场景 / 其他自定义字段)
+            if ((!textVal || textVal === defaultVal) && sku.raw && sku.raw[key] != null) {
+              textVal = sku.raw[key]
+            }
+            // 再兜底:如果 sku 上有同名字段
+            if ((!textVal || textVal === defaultVal) && sku[key] != null) {
+              textVal = sku[key]
+            }
+            return textVal
+          }
+
+          if (mode === 'multiple') {
+            // 多个货号同角度:
+            // - 按 data-key + 出现顺序把文字分配给不同货号
+            // - 如果该 slot 没有对应货号(usedSkus[slotIndex] 为 null),则隐藏该文字层
+            const keyCounter = {}
+            textPlaceholders.forEach((obj) => {
+              const key = obj['data-key']
+              if (!key) return
+              const idxForKey = keyCounter[key] || 0
+              keyCounter[key] = idxForKey + 1
+
+              const slotIndex = idxForKey % perCanvasSlots
+              const sku = usedSkus[slotIndex]
+              if (!sku) {
+                obj.visible = false
+                return
+              }
+
+              const origin = obj['data-value'] || ''
+              const textVal = mapKeyToText(sku, key, origin)
+
+              obj.visible = true
+              obj.text = textVal
+              obj['data-value'] = textVal
+            })
+          } else {
+            // 默认 / single:全部文字都使用同一个货号(默认模式只生成 1 张,用第一个货号)
+            const sku = usedSkus[0]
+            if (sku) {
+              textPlaceholders.forEach((obj) => {
+                const key = obj['data-key']
+                if (!key) return
+                const origin = obj['data-value'] || ''
+                const textVal = mapKeyToText(sku, key, origin)
+
+                obj.visible = true
+                obj.text = textVal
+                obj['data-value'] = textVal
+              })
+            }
+          }
+        }
+
+        // 创建离屏 canvas
         const el = document.createElement('canvas')
         const el = document.createElement('canvas')
         el.width = width
         el.width = width
         el.height = height
         el.height = height
@@ -398,98 +575,124 @@ export async function renderImagesByPlans(plans, canvasList, skus) {
           renderOnAddRemove: false,
           renderOnAddRemove: false,
         })
         })
 
 
-        console.log('开始加载 fabric JSON...')
+        // 添加fabric事件监听器来调试图片加载
+        let loadedImageCount = 0
+        const totalImagePlaceholders = imgPlaceholders.filter(obj => obj.visible !== false).length
         
         
-        // 添加 fabric 加载回调
+
+        // 重要:使用reviver函数确保dataURL图片正确加载
+        const reviver = (key, value, object) => {
+          // 处理图片加载
+          if (key === 'src' && value && typeof value === 'string') {
+            // 对于dataURL,确保fabric正确处理
+            return value
+          }
+          
+          // 记录图片加载状态
+          if (key === 'type' && value === 'image' && object && object.src) {
+          }
+          
+          return value
+        }
+
+        
+        // 把已经替换好动态数据的 JSON 加载进 fabric;
+        // loadFromJSON 会等所有图片资源加载完之后才调用回调函数
         fcanvas.loadFromJSON(json, () => {
         fcanvas.loadFromJSON(json, () => {
+          
           try {
           try {
-            console.log('fabric 加载完成,开始渲染')
             
             
-            // 检查实际加载的对象
-            const loadedObjects = fcanvas.getObjects()
-            console.log('实际加载的对象数:', loadedObjects.length)
+            // 检查所有加载的对象
+            const allObjects = fcanvas.getObjects()
+            
+            // 检查图片对象
+            const imageObjects = allObjects.filter(obj => obj.type === 'image')
             
             
-            loadedObjects.forEach((obj, idx) => {
+     
+            // 确保所有图片对象都正确渲染
+            fcanvas.getObjects().forEach(obj => {
               if (obj.type === 'image') {
               if (obj.type === 'image') {
-                console.log(`图片对象 ${idx}:`, {
-                  type: obj.type,
-                  src: obj.getSrc(),
-                  visible: obj.visible,
-                  width: obj.width,
-                  height: obj.height,
-                  scaleX: obj.scaleX,
-                  scaleY: obj.scaleY
-                })
+                // 如果图片有clipPath,确保它正确应用
+                if (obj.clipPath) {
+                  obj.setCoords()
+                }
+                // 确保图片在canvas边界内正确显示
+                obj.setCoords()
               }
               }
             })
             })
             
             
             fcanvas.renderAll()
             fcanvas.renderAll()
-            console.log('渲染完成,生成 dataURL')
+
+            // 获取商品图宽高,并在导出时根据宽度做压缩控制
+            const MAX_WIDTH = 2048
+            // 默认导出倍数(原来为 2,保持清晰度)
+            let exportMultiplier = 2
+
+            // 计算按当前导出倍数后的宽度
+            const expectedWidth = width * exportMultiplier
+            
+            if (expectedWidth > MAX_WIDTH) {
+              // 根据最大宽度反推需要的缩放倍数(<= 1)
+              exportMultiplier = MAX_WIDTH / width
+            }
+
+            const finalWidth = Math.round(width * exportMultiplier)
+            const finalHeight = Math.round(height * exportMultiplier)
+
             
             
             const dataUrl = fcanvas.toDataURL({
             const dataUrl = fcanvas.toDataURL({
               format: 'jpeg',
               format: 'jpeg',
-              multiplier: 2,
+              multiplier: exportMultiplier,
               enableRetinaScaling: true,
               enableRetinaScaling: true,
             })
             })
             
             
-            console.log('dataURL 生成成功,长度:', dataUrl.length)
-            
+
             fcanvas.dispose()
             fcanvas.dispose()
 
 
             resolve({
             resolve({
               canvasIndex,
               canvasIndex,
               dataUrl,
               dataUrl,
+              width: finalWidth,
+              height: finalHeight,
             })
             })
           } catch (e) {
           } catch (e) {
-            console.error('渲染回调中出错:', e)
             try {
             try {
               fcanvas.dispose()
               fcanvas.dispose()
-            } catch (e2) {}
+            } catch (e2) {
+            }
             resolve(null)
             resolve(null)
           }
           }
-        }, (obj, object) => {
-          // fabric 加载时的回调
-          if (object && object.type === 'image') {
-            console.log('正在加载图片对象:', object.getSrc())
-            object.set({
-              crossOrigin: 'anonymous'
-            })
-          }
+        }, reviver)
+        
+        // 添加fabric错误处理
+        fcanvas.on('object:added', (e) => {
         })
         })
+        
       } catch (e) {
       } catch (e) {
-        console.error('渲染过程中出错:', e)
         resolve(null)
         resolve(null)
       }
       }
     })
     })
+  }
 
 
   for (const plan of plans || []) {
   for (const plan of plans || []) {
     const canvasItem = canvasList[plan.canvasIndex]
     const canvasItem = canvasList[plan.canvasIndex]
-    if (!canvasItem) {
-      console.warn(`plan 中 canvasIndex ${plan.canvasIndex} 在 canvasList 中不存在`)
-      continue
-    }
+    if (!canvasItem) continue
 
 
-    console.log(`\n处理 plan ${plan.canvasIndex},图片数: ${plan.images?.length}`)
     
     
     for (const imgPlan of plan.images || []) {
     for (const imgPlan of plan.images || []) {
-      console.log(`  生成图片,skuIndexes: ${imgPlan.skuIndexes}`)
       // eslint-disable-next-line no-await-in-loop
       // eslint-disable-next-line no-await-in-loop
       const res = await renderOne(canvasItem, plan, imgPlan)
       const res = await renderOne(canvasItem, plan, imgPlan)
       if (res) {
       if (res) {
-        console.log(`  图片生成成功: canvasIndex=${res.canvasIndex}`)
         results.push(res)
         results.push(res)
       } else {
       } else {
-        console.log(`  图片生成失败`)
-      } 
+      }
     }
     }
   }
   }
-
-  console.log('=== 图片生成结束 ===')
-  console.log('成功生成图片数:', results.length)
   return results
   return results
 }
 }
 
 
 
 
+
 /**
 /**
  * 针对每个款号(style),按画布生成所有图片,并额外生成「所有画布组合在一起」的一张长图。
  * 针对每个款号(style),按画布生成所有图片,并额外生成「所有画布组合在一起」的一张长图。
  *
  *
@@ -506,63 +709,128 @@ export async function generateAllStyleImageBundles(canvasList, goodsList) {
   const bundles = []
   const bundles = []
 
 
   // 工具:把若干 dataURL 竖向拼接为一张长图
   // 工具:把若干 dataURL 竖向拼接为一张长图
-  const composeCombinedImage = (images) =>
-    new Promise((resolve) => {
-      const validImages = (images || []).filter(
-        img => img && typeof img.dataUrl === 'string' && img.dataUrl.startsWith('data:image')
+  // const composeCombinedImage = (images) =>
+  //   new Promise((resolve) => {
+  //     const validImages = (images || []).filter(
+  //       img => img && typeof img.dataUrl === 'string' && img.dataUrl.startsWith('data:image')
+  //     )
+  //     if (!validImages.length) return resolve(null)
+
+  //     Promise.all(
+  //       validImages.map(img =>
+  //         new Promise(res => {
+  //           fabric.Image.fromURL(
+  //             img.dataUrl,
+  //             (oImg) => res(oImg),
+  //             { crossOrigin: 'anonymous' }
+  //           )
+  //         })
+  //       )
+  //     ).then(fabricImages => {
+  //       const widths = fabricImages.map(i => i.width * (i.scaleX || 1))
+  //       const heights = fabricImages.map(i => i.height * (i.scaleY || 1))
+  //       const totalHeight = heights.reduce((a, b) => a + b, 0)
+  //       const maxWidth = widths.reduce((a, b) => Math.max(a, b), 0)
+
+  //       const el = document.createElement('canvas')
+  //       el.width = maxWidth
+  //       el.height = totalHeight
+
+  //       const canvas = new fabric.Canvas(el, {
+  //         backgroundColor: '#fff',
+  //         width: maxWidth,
+  //         height: totalHeight,
+  //         renderOnAddRemove: false,
+  //       })
+
+  //       let currentTop = 0
+  //       fabricImages.forEach((img, idx) => {
+  //         const w = widths[idx]
+  //         const h = heights[idx]
+  //         img.set({
+  //           left: (maxWidth - w) / 2,
+  //           top: currentTop,
+  //         })
+  //         currentTop += h
+  //         canvas.add(img)
+  //       })
+
+  //       canvas.renderAll()
+  //       const dataUrl = canvas.toDataURL({
+  //         format: 'jpeg',
+  //         multiplier:2,
+  //         enableRetinaScaling: true,
+  //       })
+  //       const result = { dataUrl, width: maxWidth, height: totalHeight }
+  //       canvas.dispose()
+  //       resolve(result)
+  //     }).catch(() => resolve(null))
+  //   })
+  // 在 generateAllStyleImageBundles 函数中修复
+const composeCombinedImage = (images) =>
+  new Promise((resolve) => {
+    const validImages = (images || []).filter(
+      img => img && typeof img.dataUrl === 'string' && img.dataUrl.startsWith('data:image')
+    )
+    if (!validImages.length) return resolve(null)
+
+    // 确保有canvas元素
+    const el = document.createElement('canvas')
+    if (!el) return resolve(null)
+    
+    const ctx = el.getContext('2d')
+    if (!ctx) {
+      el.remove()
+      return resolve(null)
+    }
+
+    Promise.all(
+      validImages.map(img =>
+        new Promise(res => {
+          const tempImg = new Image()
+          tempImg.crossOrigin = 'anonymous'
+          tempImg.onload = () => res({ img: tempImg, width: tempImg.width, height: tempImg.height })
+          tempImg.onerror = () => res(null)
+          tempImg.src = img.dataUrl
+        })
       )
       )
-      if (!validImages.length) return resolve(null)
-
-      Promise.all(
-        validImages.map(img =>
-          new Promise(res => {
-            fabric.Image.fromURL(
-              img.dataUrl,
-              (oImg) => res(oImg),
-              { crossOrigin: 'anonymous' }
-            )
-          })
-        )
-      ).then(fabricImages => {
-        const widths = fabricImages.map(i => i.width * (i.scaleX || 1))
-        const heights = fabricImages.map(i => i.height * (i.scaleY || 1))
-        const totalHeight = heights.reduce((a, b) => a + b, 0)
-        const maxWidth = widths.reduce((a, b) => Math.max(a, b), 0)
+    ).then(imageInfos => {
+      const validInfos = imageInfos.filter(info => info !== null)
+      if (!validInfos.length) {
+        el.remove()
+        return resolve(null)
+      }
 
 
-        const el = document.createElement('canvas')
-        el.width = maxWidth
-        el.height = totalHeight
+      const widths = validInfos.map(info => info.width)
+      const heights = validInfos.map(info => info.height)
+      const totalHeight = heights.reduce((a, b) => a + b, 0)
+      const maxWidth = widths.reduce((a, b) => Math.max(a, b), 0)
 
 
-        const canvas = new fabric.Canvas(el, {
-          backgroundColor: '#fff',
-          width: maxWidth,
-          height: totalHeight,
-          renderOnAddRemove: false,
-        })
+      el.width = maxWidth
+      el.height = totalHeight
 
 
-        let currentTop = 0
-        fabricImages.forEach((img, idx) => {
-          const w = widths[idx]
-          const h = heights[idx]
-          img.set({
-            left: (maxWidth - w) / 2,
-            top: currentTop,
-          })
-          currentTop += h
-          canvas.add(img)
-        })
+      ctx.fillStyle = '#fff'
+      ctx.fillRect(0, 0, maxWidth, totalHeight)
 
 
-        canvas.renderAll()
-        const dataUrl = canvas.toDataURL({
-          format: 'jpeg',
-          multiplier:2,
-          enableRetinaScaling: true,
-        })
-        const result = { dataUrl, width: maxWidth, height: totalHeight }
-        canvas.dispose()
-        resolve(result)
-      }).catch(() => resolve(null))
+      let currentTop = 0
+      validInfos.forEach((info, idx) => {
+        const w = widths[idx]
+        const h = heights[idx]
+        const left = (maxWidth - w) / 2
+        ctx.drawImage(info.img, left, currentTop, w, h)
+        currentTop += h
+      })
+
+      const dataUrl = el.toDataURL('image/jpeg', 0.9)
+      const result = { dataUrl, width: maxWidth, height: totalHeight }
+      el.remove()
+      resolve(result)
+    }).catch((error) => {
+      console.error('组合图片失败:', error)
+      if (el) el.remove()
+      resolve(null)
     })
     })
+  })
 
 
   // 按款号分组处理,避免不同款号的货号混在一起
   // 按款号分组处理,避免不同款号的货号混在一起
   for (const group of goodsList || []) {
   for (const group of goodsList || []) {

+ 13 - 1
frontend/src/views/components/marketingEdit/goods4.json

@@ -1,5 +1,5 @@
 {
 {
-  "AQN11119": {
+  "AQG1411283": {
     "款号": "AQG1411283",
     "款号": "AQG1411283",
     "货号资料": [
     "货号资料": [
       {
       {
@@ -13,6 +13,18 @@
           "内里": "C:\\Users\\Administrator\\Desktop\\img\\AQG1411283\\AQG1411283(5).png"
           "内里": "C:\\Users\\Administrator\\Desktop\\img\\AQG1411283\\AQG1411283(5).png"
         },
         },
         "设计理念": "优先使用柔软透气的天然皮革(如头层牛皮、猪皮),并搭配记忆棉鞋垫、透气内里等科技材料,提升穿着体验。"
         "设计理念": "优先使用柔软透气的天然皮革(如头层牛皮、猪皮),并搭配记忆棉鞋垫、透气内里等科技材料,提升穿着体验。"
+      },
+      {
+        "货号": "AQG1411284",
+        "颜色": "黑色",
+        "pics": {
+          "俯视": "C:\\Users\\Administrator\\Desktop\\img\\AQG1411283\\AQG1411283(1).png",
+          "侧视": "C:\\Users\\Administrator\\Desktop\\img\\AQG1411283\\AQG1411283(2).png",
+          "后跟": "C:\\Users\\Administrator\\Desktop\\img\\AQG1411283\\AQG1411283(3).png",
+          "鞋底": "C:\\Users\\Administrator\\Desktop\\img\\AQG1411283\\AQG1411283(4).png",
+          "内里": "C:\\Users\\Administrator\\Desktop\\img\\AQG1411283\\AQG1411283(5).png"
+        },
+        "设计理念": "优先使用柔软透气的天然皮革(如头层牛皮、猪皮),并搭配记忆棉鞋垫、透气内里等科技材料,提升穿着体验。"
       }
       }
     ]
     ]
   }
   }

+ 20 - 0
frontend/src/views/components/marketingEdit/goods5.json

@@ -0,0 +1,20 @@
+{
+  "AQG1411283KT": {
+    "款号": "AQG1411283KT",
+    "货号资料": [
+      {
+        "货号": "AQG1411283KT",
+        "颜色": "灰色",
+        "pics": {
+          "俯视": "C:\\Users\\Administrator\\Desktop\\img\\AQG1411283KT\\AQG1411283(1).png",
+          "侧视": "C:\\Users\\Administrator\\Desktop\\img\\AQG1411283KT\\AQG1411283(2).png",
+          "后跟": "C:\\Users\\Administrator\\Desktop\\img\\AQG1411283KT\\AQG1411283(3).png",
+          "鞋底": "C:\\Users\\Administrator\\Desktop\\img\\AQG1411283KT\\AQG1411283(4).png",
+          "内里": "C:\\Users\\Administrator\\Desktop\\img\\AQG1411283KT\\AQG1411283(5).png"
+        },
+        "设计理念": "优先使用柔软透气的天然皮革(如头层牛皮、猪皮),并搭配记忆棉鞋垫、透气内里等科技材料,提升穿着体验。"
+      }
+
+    ]
+  }
+}

+ 6 - 3
frontend/src/views/components/marketingEdit/index.vue

@@ -17,6 +17,7 @@ import goods from './goods.json'
 import goods1 from './goods1.json'
 import goods1 from './goods1.json'
 import goods2 from './goods2.json'
 import goods2 from './goods2.json'
 import goods4 from './goods4.json'
 import goods4 from './goods4.json'
+import goods5 from './goods5.json'
 import canvas from './canvas.json'
 import canvas from './canvas.json'
 import { buildRenderPlans, normalizeGoods } from './generateImagesPlan'
 import { buildRenderPlans, normalizeGoods } from './generateImagesPlan'
 import { renderImagesByPlans, generateAllStyleImageBundles } from './generateImagesRender'
 import { renderImagesByPlans, generateAllStyleImageBundles } from './generateImagesRender'
@@ -538,7 +539,9 @@ export default {
         JSON.parse(JSON.stringify(goods)), 
         JSON.parse(JSON.stringify(goods)), 
         // JSON.parse(JSON.stringify(goods1)), 
         // JSON.parse(JSON.stringify(goods1)), 
         // JSON.parse(JSON.stringify(goods2)),
         // JSON.parse(JSON.stringify(goods2)),
-        JSON.parse(JSON.stringify(goods4))]
+        JSON.parse(JSON.stringify(goods4)),
+        JSON.parse(JSON.stringify(goods5))
+      ]
         
         
        const canvasJson = JSON.parse(JSON.stringify(canvas))
        const canvasJson = JSON.parse(JSON.stringify(canvas))
 
 
@@ -747,8 +750,8 @@ export default {
       margin-right: 5px;
       margin-right: 5px;
     }
     }
     .icon-img {
     .icon-img {
-      width: 30px;
-      height: 30px;
+      width: 34px;
+      // height: 30px;
       border-radius: 5px;
       border-radius: 5px;
       margin-right: 10px;
       margin-right: 10px;
     }
     }