Procházet zdrojové kódy

feat(api): 增强文件下载功能与模板处理

- 在 http.ts 中新增 DOWNLOAD 方法,支持文件下载并自动处理文件后缀。
- 在 other.ts 中添加 downlaodCustomerTemplate 方法,允许用户下载自定义模板。
- 更新 Photography 视图,集成下载功能并优化服务标签页的交互逻辑。
- 在模板编辑页面中,扩展保存逻辑以包含 Excel 模板头信息。
- 优化图片生成逻辑,增强调试信息输出,便于后续问题排查。
kongwenhao před 5 dny
rodič
revize
4fe8a63aa2

+ 9 - 2
frontend/src/apis/other.ts

@@ -1,4 +1,4 @@
-import { GET,POST,UPLOAD  } from "@/utils/http";
+import { GET,POST,UPLOAD ,DOWNLOAD  } from "@/utils/http";
 // import type { UserRequest } from "@/apis/types/user";
 
 
@@ -46,4 +46,11 @@ export async function saveCustomerTemplate(params:any){
 // 删除自定义模板
 export async function deleteCustomerTemplate(params: { id: number | string }) {
     return POST('/api/ai_image/auto_photo/delete_template', params)
-}
+}
+
+
+// 删除自定义模板
+
+export async function downlaodCustomerTemplate(params: { id: number , filename: string }){
+    return DOWNLOAD('/api/ai_image/auto_photo/template_excel', params , { filename: params.filename })
+}

+ 2 - 1
frontend/src/router/index.ts

@@ -16,7 +16,8 @@ const routes: RouteRecordRaw[] = [
     {
         path: "/home",
         name: "home",
-        component: () => import("@/views/Home/index.vue"),
+        // component: () => import("@/views/Home/index.vue"),
+        component: () => import("@/views/Photography/processImage.vue"),
         meta: {
             title: '首页',
             noAuth: true,

+ 130 - 0
frontend/src/utils/http.ts

@@ -134,6 +134,11 @@ service.interceptors.response.use(
             loadingClose(response.config.requestId as string);
         }
 
+        // 如果是文件流(blob)响应,返回整个响应对象以便访问响应头
+        if (res instanceof Blob) {
+            return response;
+        }
+
         // 如果自定义状态码不为0,则判断为错误
         if (res.code !== 0) {
             switch (res.code) {
@@ -249,6 +254,131 @@ export function GET<T>(url: string, data?: any, config?: any): Promise<T> {
 }
 
 /**
+ * 根据 MIME 类型获取文件后缀
+ * @param {string} mimeType MIME 类型
+ * @returns {string} 文件后缀(包含点号,如 .xlsx)
+ */
+function getFileExtensionFromMimeType(mimeType: string): string {
+    const mimeToExt: Record<string, string> = {
+        // Excel 文件
+        'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
+        'application/vnd.ms-excel': '.xls',
+        'application/excel': '.xls',
+        'text/xlsx': '.xlsx',
+        // Word 文件
+        'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
+        'application/msword': '.doc',
+        // PDF 文件
+        'application/pdf': '.pdf',
+        // 图片文件
+        'image/jpeg': '.jpg',
+        'image/jpg': '.jpg',
+        'image/png': '.png',
+        'image/gif': '.gif',
+        'image/webp': '.webp',
+        // 压缩文件
+        'application/zip': '.zip',
+        'application/x-rar-compressed': '.rar',
+        'application/x-7z-compressed': '.7z',
+        // 文本文件
+        'text/plain': '.txt',
+        'text/csv': '.csv',
+        'text/html': '.html',
+        // 其他
+        'application/json': '.json',
+        'application/xml': '.xml',
+    };
+    
+    return mimeToExt[mimeType] || '';
+}
+
+/**
+ * 确保文件名有正确的后缀
+ * @param {string} filename 原始文件名
+ * @param {string} mimeType MIME 类型
+ * @returns {string} 带后缀的文件名
+ */
+function ensureFileExtension(filename: string, mimeType: string): string {
+    // 如果文件名已经有后缀,直接返回
+    if (filename.includes('.')) {
+        return filename;
+    }
+    
+    // 根据 MIME 类型添加后缀
+    const extension = getFileExtensionFromMimeType(mimeType);
+    return filename + extension;
+}
+
+/**
+ * 发起文件下载请求
+ *
+ * @param {string} url 请求的 URL
+ * @param {any} [data] 请求参数
+ * @param {any} [config] 请求配置
+ * @param {string} [config.filename] 下载的文件名(可选,如果不提供则使用当前时间戳)
+ * @param {boolean} [config.loading] 是否显示加载动画,默认为 true
+ * @param {boolean} [config.showErrorMessage] 是否显示错误消息,默认为 true
+ * @returns {Promise<Blob>} 返回一个 Promise,解析为文件 Blob 对象
+ */
+export function DOWNLOAD(url: string, data?: any, config?: { filename?: string; loading?: boolean; showErrorMessage?: boolean }): Promise<Blob> {
+    return service.get(url, {
+        params: data,
+        responseType: 'blob',
+        loading: config?.loading ?? true,
+        showErrorMessage: config?.showErrorMessage ?? true,
+    }).then((response: AxiosResponse) => {
+        // 响应拦截器对 blob 响应返回整个 response 对象
+        const blob = response.data as Blob;
+        
+        // 检查是否是错误响应(通常错误响应是 JSON 格式的 blob)
+        if (blob.type && blob.type.includes('application/json')) {
+            // 如果是 JSON,可能是错误信息,尝试读取并提示
+            blob.text().then((text: string) => {
+                try {
+                    const errorData = JSON.parse(text);
+                    if (errorData.message) {
+                        Message({
+                            message: errorData.message || '下载失败',
+                            type: 'error',
+                            duration: 5 * 1000,
+                        });
+                    }
+                } catch (e) {
+                    Message({
+                        message: '下载失败',
+                        type: 'error',
+                        duration: 5 * 1000,
+                    });
+                }
+            });
+            return Promise.reject(new Error('下载失败:服务器返回错误信息'));
+        }
+        console.log('blob', blob);
+        
+        // 获取文件名:优先使用配置中的文件名,否则使用当前时间戳
+        let filename = config?.filename || `${Date.now()}`;
+        
+        // 根据文件类型自动添加文件后缀(如果还没有后缀)
+        if (blob.type) {
+            filename = ensureFileExtension(filename, blob.type);
+        }
+        
+        // 创建下载链接并触发下载
+        const downloadUrl = window.URL.createObjectURL(blob);
+        const link = document.createElement('a');
+        link.href = downloadUrl;
+        link.download = filename;
+        document.body.appendChild(link);
+        link.click();
+        document.body.removeChild(link);
+        window.URL.revokeObjectURL(downloadUrl);
+        
+        return blob;
+    });
+}
+
+
+/**
  * 发起 POST 请求
  *
  * @template T 泛型,表示返回数据的类型

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 227 - 321
frontend/src/views/Photography/detail.vue


+ 9 - 4
frontend/src/views/Tpl/Edit/index.vue

@@ -165,12 +165,17 @@ const doSave = async (payload: any) => {
     } 
     requestData.customer_template_images = goodsImages.value
     requestData.template_image_order =  route.query.template_image_order as string | undefined
-
-    console.log('requestData' , requestData)
-
+    let template_excel_headers = []
+    requestData.customer_template_json.map(item =>  {
+      const canvas_json =  JSON.parse( item.canvas_json )
+      canvas_json.objects.map(itm => {
+        if(itm.type =="textbox" && itm.text) 
+        template_excel_headers.push(itm.text)
+      })
+    })
+    requestData.template_excel_headers = template_excel_headers
     await saveCustomerTemplate(requestData)
     ElMessage.success(isEdit ? '模板保存成功' : '模板创建成功')
-    // 保存成功后返回上一页(模板列表或详情页面)
     router.back()
   } catch (error: any) {
     console.error('保存模板失败:', error)

+ 348 - 88
frontend/src/views/components/marketingEdit/generateImagesRender.js

@@ -13,41 +13,271 @@ import { buildRenderPlans, normalizeGoods } from './generateImagesPlan'
  * @param {Array} skus normalizeGoods(goodsList) 的结果
  * @returns {Promise<Array<{canvasIndex:number,dataUrl:string}>>}
  */
+// export async function renderImagesByPlans(plans, canvasList, skus) {
+//   const results = []
+
+//   // 工具:从 canvas_json 创建一个离屏 fabric.Canvas,并应用一次性的渲染逻辑
+//   const renderOne = (canvasItem, planForCanvas, imagePlan) =>
+//     new Promise((resolve) => {
+//       if (!canvasItem) {
+//         return resolve(null)
+//       }
+
+//       const { canvasIndex } = planForCanvas
+//       const { skuIndexes } = imagePlan
+
+//       // 模特/场景占位:直接返回类型,不渲染
+//       if (canvasItem.canvas_json === 'model') {
+//         return resolve({
+//           canvasIndex,
+//           dataUrl: 'model',
+//         })
+//       }
+//       if (canvasItem.canvas_json === 'scene') {
+//         return resolve({
+//           canvasIndex,
+//           dataUrl: 'scene',
+//         })
+//       }
+
+//       // 解析 JSON 为可修改的对象
+//       let json
+//       try {
+//         json = typeof canvasItem.canvas_json === 'string'
+//           ? JSON.parse(canvasItem.canvas_json)
+//           : JSON.parse(JSON.stringify(canvasItem.canvas_json))
+//       } catch (e) {
+//         console.warn('[generateImagesRender] parse canvas_json failed', e)
+//         return resolve(null)
+//       }
+
+//       const width = Number(canvasItem.width) || 395
+//       const height = Number(canvasItem.height) || 600
+//       const bgColor = canvasItem.bg_color || '#fff'
+
+//       try {
+//         const mode = canvasItem.multi_goods_mode || ''
+//         const perCanvasSlots = planForCanvas.perCanvasSlots || 1
+//         const usedSkus = (skuIndexes || []).map((idx) => (idx != null ? skus[idx] : null))
+
+//         // 先在原始 JSON 上做数据替换,避免 setSrc 的异步问题;
+//         // loadFromJSON 会在所有图片加载完成后才触发回调。
+//         const objs = (json && Array.isArray(json.objects)) ? json.objects : []
+
+//         // 1) 处理图片占位(data-type = img)
+//         const imgPlaceholders = objs.filter((o) => o && o['data-type'] === 'img')
+
+//         if (mode === 'multiple') {
+//           // 多个货号同角度:同一画布中,有多个 img 占位,按顺序对应 skuIndexes
+//           imgPlaceholders.forEach((obj, idx) => {
+//             const slotIndex = idx % perCanvasSlots
+//             const sku = usedSkus[slotIndex]
+//             if (!sku) {
+//               obj.visible = false
+//               return
+//             }
+//             const angleKey = obj['data-key']
+//             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) => {
+//             if (!sku) {
+//               obj.visible = false
+//               return
+//             }
+//             const angleKey = obj['data-key']
+//             const url = (sku.pics && sku.pics[angleKey]) || ''
+//             if (!url) {
+//               obj.visible = false
+//               return
+//             }
+//             obj.visible = true
+//             obj['data-value'] = url
+//             obj.src = url
+//           })
+//         }
+
+//         // 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')
+//         el.width = width
+//         el.height = height
+
+//         const fcanvas = new fabric.Canvas(el, {
+//           backgroundColor: bgColor,
+//           width,
+//           height,
+//           renderOnAddRemove: false,
+//         })
+
+//         // 把已经替换好动态数据的 JSON 加载进 fabric;
+//         // loadFromJSON 会等所有图片资源加载完之后才调用回调函数
+//         fcanvas.loadFromJSON(json, () => {
+//           try {
+//             fcanvas.renderAll()
+//             const dataUrl = fcanvas.toDataURL({
+//               format: 'jpeg',
+//               multiplier:2,
+//               enableRetinaScaling: true,
+//             })
+
+//             fcanvas.dispose()
+
+//             resolve({
+//               canvasIndex,
+//               dataUrl,
+//             })
+//           } catch (e) {
+//             console.warn('[generateImagesRender] render one failed in callback', e)
+//             try {
+//               fcanvas.dispose()
+//             } catch (e2) {}
+//             resolve(null)
+//           }
+//         })
+//       } catch (e) {
+//         console.warn('[generateImagesRender] render one failed', e)
+//         resolve(null)
+//       }
+//     })
+
+//   for (const plan of plans || []) {
+//     const canvasItem = canvasList[plan.canvasIndex]
+//     if (!canvasItem) continue
+
+//     for (const imgPlan of plan.images || []) {
+//       // eslint-disable-next-line no-await-in-loop
+//       const res = await renderOne(canvasItem, plan, imgPlan)
+//       if (res) results.push(res)
+//     }
+//   }
+
+//   return results
+// }
 export async function renderImagesByPlans(plans, canvasList, skus) {
+  console.log('=== 开始生成图片 ===')
+  console.log('skus 数据:', skus)
+  console.log('canvasList:', canvasList)
+  console.log('plans:', plans)
+  
   const results = []
 
-  // 工具:从 canvas_json 创建一个离屏 fabric.Canvas,并应用一次性的渲染逻辑
   const renderOne = (canvasItem, planForCanvas, imagePlan) =>
     new Promise((resolve) => {
       if (!canvasItem) {
+        console.warn('canvasItem 为空')
         return resolve(null)
       }
 
       const { canvasIndex } = planForCanvas
       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') {
+        console.log('跳过模特占位')
         return resolve({
           canvasIndex,
           dataUrl: 'model',
         })
       }
       if (canvasItem.canvas_json === 'scene') {
+        console.log('跳过场景占位')
         return resolve({
           canvasIndex,
           dataUrl: 'scene',
         })
       }
 
-      // 解析 JSON 为可修改的对象
+      // 解析 JSON
       let json
       try {
         json = typeof canvasItem.canvas_json === 'string'
           ? JSON.parse(canvasItem.canvas_json)
           : JSON.parse(JSON.stringify(canvasItem.canvas_json))
+        console.log('解析 canvas_json 成功')
       } catch (e) {
-        console.warn('[generateImagesRender] parse canvas_json failed', e)
+        console.error('解析 canvas_json 失败:', e)
+        console.log('原始 canvas_json:', canvasItem.canvas_json)
         return resolve(null)
       }
 
@@ -58,122 +288,105 @@ export async function renderImagesByPlans(plans, canvasList, skus) {
       try {
         const mode = canvasItem.multi_goods_mode || ''
         const perCanvasSlots = planForCanvas.perCanvasSlots || 1
-        const usedSkus = (skuIndexes || []).map((idx) => (idx != null ? skus[idx] : null))
+        const usedSkus = (skuIndexes || []).map((idx) => {
+          const sku = idx != null ? skus[idx] : null
+          if (sku) {
+            console.log(`货号 ${sku.sku} 的图片:`, sku.pics)
+          }
+          return sku
+        })
 
-        // 先在原始 JSON 上做数据替换,避免 setSrc 的异步问题;
-        // loadFromJSON 会在所有图片加载完成后才触发回调。
-        const objs = (json && Array.isArray(json.objects)) ? json.objects : []
+        console.log('mode:', mode)
+        console.log('perCanvasSlots:', perCanvasSlots)
+        console.log('实际使用的 sku:', usedSkus.map(s => s?.sku))
 
-        // 1) 处理图片占位(data-type = img)
+        // 处理图片占位
+        const objs = (json && Array.isArray(json.objects)) ? json.objects : []
+        console.log('总对象数:', objs.length)
+        
         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']
+          })
+        })
 
         if (mode === 'multiple') {
-          // 多个货号同角度:同一画布中,有多个 img 占位,按顺序对应 skuIndexes
+          console.log('使用 multiple 模式')
           imgPlaceholders.forEach((obj, idx) => {
             const slotIndex = idx % perCanvasSlots
             const sku = usedSkus[slotIndex]
+            console.log(`占位符 ${idx} -> 货位 ${slotIndex} -> 货号:`, sku?.sku)
+            
             if (!sku) {
+              console.log(`货位 ${slotIndex} 无货号,隐藏图层`)
               obj.visible = false
               return
             }
+            
             const angleKey = obj['data-key']
             const url = (sku.pics && sku.pics[angleKey]) || ''
+            console.log(`角度 ${angleKey} 的图片URL:`, url)
+            
             if (!url) {
+              console.log(`货号 ${sku.sku} 无角度 ${angleKey} 图片,隐藏图层`)
               obj.visible = false
               return
             }
+            
+            console.log(`设置图片 ${idx}: ${angleKey} = ${url}`)
             obj.visible = true
             obj['data-value'] = url
             obj.src = url
+            
+            // 在 Electron 中,需要设置 crossOrigin
+            if (window.electron) {
+              obj.crossOrigin = 'anonymous'
+            }
           })
         } else {
-          // 默认 / single:一个货号多角度,一张图只用一个货号
+          console.log('使用 single/默认模式')
           const sku = usedSkus[0]
-          imgPlaceholders.forEach((obj) => {
+          console.log('使用的货号:', sku?.sku)
+          
+          imgPlaceholders.forEach((obj, idx) => {
             if (!sku) {
+              console.log('无货号,隐藏所有图层')
               obj.visible = false
               return
             }
+            
             const angleKey = obj['data-key']
             const url = (sku.pics && sku.pics[angleKey]) || ''
+            console.log(`图片 ${idx} 角度 ${angleKey}:`, url)
+            
             if (!url) {
+              console.log(`无 ${angleKey} 图片,隐藏图层`)
               obj.visible = false
               return
             }
+            
+            console.log(`设置图片 ${idx}: ${angleKey} = ${url}`)
             obj.visible = true
             obj['data-value'] = url
             obj.src = url
-          })
-        }
-
-        // 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]
+            
+            if (window.electron) {
+              obj.crossOrigin = 'anonymous'
             }
-            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
+        // 创建 canvas
         const el = document.createElement('canvas')
         el.width = width
         el.height = height
@@ -185,17 +398,42 @@ export async function renderImagesByPlans(plans, canvasList, skus) {
           renderOnAddRemove: false,
         })
 
-        // 把已经替换好动态数据的 JSON 加载进 fabric;
-        // loadFromJSON 会等所有图片资源加载完之后才调用回调函数
+        console.log('开始加载 fabric JSON...')
+        
+        // 添加 fabric 加载回调
         fcanvas.loadFromJSON(json, () => {
           try {
+            console.log('fabric 加载完成,开始渲染')
+            
+            // 检查实际加载的对象
+            const loadedObjects = fcanvas.getObjects()
+            console.log('实际加载的对象数:', loadedObjects.length)
+            
+            loadedObjects.forEach((obj, idx) => {
+              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
+                })
+              }
+            })
+            
             fcanvas.renderAll()
+            console.log('渲染完成,生成 dataURL')
+            
             const dataUrl = fcanvas.toDataURL({
               format: 'jpeg',
-              multiplier:2,
+              multiplier: 2,
               enableRetinaScaling: true,
             })
-
+            
+            console.log('dataURL 生成成功,长度:', dataUrl.length)
+            
             fcanvas.dispose()
 
             resolve({
@@ -203,33 +441,55 @@ export async function renderImagesByPlans(plans, canvasList, skus) {
               dataUrl,
             })
           } catch (e) {
-            console.warn('[generateImagesRender] render one failed in callback', e)
+            console.error('渲染回调中出错:', e)
             try {
               fcanvas.dispose()
             } catch (e2) {}
             resolve(null)
           }
+        }, (obj, object) => {
+          // fabric 加载时的回调
+          if (object && object.type === 'image') {
+            console.log('正在加载图片对象:', object.getSrc())
+            object.set({
+              crossOrigin: 'anonymous'
+            })
+          }
         })
       } catch (e) {
-        console.warn('[generateImagesRender] render one failed', e)
+        console.error('渲染过程中出错:', e)
         resolve(null)
       }
     })
 
   for (const plan of plans || []) {
     const canvasItem = canvasList[plan.canvasIndex]
-    if (!canvasItem) continue
+    if (!canvasItem) {
+      console.warn(`plan 中 canvasIndex ${plan.canvasIndex} 在 canvasList 中不存在`)
+      continue
+    }
 
+    console.log(`\n处理 plan ${plan.canvasIndex},图片数: ${plan.images?.length}`)
+    
     for (const imgPlan of plan.images || []) {
+      console.log(`  生成图片,skuIndexes: ${imgPlan.skuIndexes}`)
       // eslint-disable-next-line no-await-in-loop
       const res = await renderOne(canvasItem, plan, imgPlan)
-      if (res) results.push(res)
+      if (res) {
+        console.log(`  图片生成成功: canvasIndex=${res.canvasIndex}`)
+        results.push(res)
+      } else {
+        console.log(`  图片生成失败`)
+      } 
     }
   }
 
+  console.log('=== 图片生成结束 ===')
+  console.log('成功生成图片数:', results.length)
   return results
 }
 
+
 /**
  * 针对每个款号(style),按画布生成所有图片,并额外生成「所有画布组合在一起」的一张长图。
  *

+ 19 - 0
frontend/src/views/components/marketingEdit/goods4.json

@@ -0,0 +1,19 @@
+{
+  "AQN11119": {
+    "款号": "AQG1411283",
+    "货号资料": [
+      {
+        "货号": "AQG1411283",
+        "颜色": "灰色",
+        "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"
+        },
+        "设计理念": "优先使用柔软透气的天然皮革(如头层牛皮、猪皮),并搭配记忆棉鞋垫、透气内里等科技材料,提升穿着体验。"
+      }
+    ]
+  }
+}

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

@@ -16,6 +16,7 @@ import icpList from '@/utils/ipc'
 import goods from './goods.json'
 import goods1 from './goods1.json'
 import goods2 from './goods2.json'
+import goods4 from './goods4.json'
 import canvas from './canvas.json'
 import { buildRenderPlans, normalizeGoods } from './generateImagesPlan'
 import { renderImagesByPlans, generateAllStyleImageBundles } from './generateImagesRender'
@@ -502,6 +503,7 @@ export default {
       // 先确保当前激活画布的快照是最新的
       this.saveCanvasSnapshot()
       // 上传每个画布的预览图,生成线上地址
+      console.log('this.data' , this.data)
       const payload = await Promise.all(this.data.map(async (item, idx) => {
         let previewUrl = item.preview || ''
         if(item.preview && typeof item.preview === 'string' && item.preview.indexOf('data:') === 0){
@@ -534,8 +536,10 @@ export default {
        // 1. 组装数据源(后续可以从接口/其他页面传入)
        const goodsData = [
         JSON.parse(JSON.stringify(goods)), 
-        JSON.parse(JSON.stringify(goods1)), 
-        JSON.parse(JSON.stringify(goods2))]
+        // JSON.parse(JSON.stringify(goods1)), 
+        // JSON.parse(JSON.stringify(goods2)),
+        JSON.parse(JSON.stringify(goods4))]
+        
        const canvasJson = JSON.parse(JSON.stringify(canvas))
 
        // 2. 针对每个款号,生成所有画布图片 + 组合长图(在内存中生成 dataURL)

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů