Explorar o código

Merge remote-tracking branch 'origin/feature-frontend' into feature-frontend

# Conflicts:
#	frontend/src/views/components/marketingEdit/tpl/header.js
panqiuyao hai 5 días
pai
achega
9a87e39751

+ 8 - 0
electron/api/generate.js

@@ -32,4 +32,12 @@ module.exports = {
       data: data
     })
   },
+
+  // 根据货号获取商品图片 JSON(生成商品模板用)
+  getGoodsImageJson(data){
+    return get({
+      url: '/get_goods_image_json',
+      data: data,
+    })
+  },
 }

+ 15 - 1
electron/controller/generate.js

@@ -5,7 +5,7 @@ const Log = require('ee-core/log');
 const Services = require('ee-core/services');
 const path = require('path');
 const fs = require('fs');
-const { generatePhotoDetail, getLogoList, addLogo,deleteLogo } = require('../api/generate');
+const { generatePhotoDetail, getLogoList, addLogo, deleteLogo, getGoodsImageJson } = require('../api/generate');
 
 const errData = {
   msg :'请求失败,请联系管理员',
@@ -77,6 +77,20 @@ class GenerateController extends Controller {
     }
   }
 
+  /**
+   * 根据货号获取商品图片 JSON(生成商品模板用)
+   */
+  async getGoodsImageJson(args) {
+    try {
+      const result = await getGoodsImageJson(args);
+      if (result.data) return result.data;
+      return errData;
+    } catch (error) {
+      Log.error('获取商品图片 JSON 失败:', error);
+      return errData;
+    }
+  }
+
 
 }
 

+ 20 - 1
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";
 
 
@@ -35,3 +35,22 @@ export function uploadImg(data,loading=true) {
         loading: loading,
     })
 }
+
+
+
+// 保存客户模版
+export async function saveCustomerTemplate(params:any){
+    return POST('/api/ai_image/auto_photo/save_customer_template', params)
+}
+
+// 删除自定义模板
+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 泛型,表示返回数据的类型

+ 1 - 0
frontend/src/utils/ipc.ts

@@ -50,6 +50,7 @@ const icpList = {
         addLogo: 'controller.generate.addLogo',
         getLogoList: 'controller.generate.getLogoList',
         deleteLogo: 'controller.generate.deleteLogo',
+        getGoodsImageJson: 'controller.generate.getGoodsImageJson',
     },
     ota:{
         updateVersion: 'controller.ota.updateVersion'

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 468 - 216
frontend/src/views/Photography/detail.vue


+ 1 - 0
frontend/src/views/Photography/mixin/usePhotography.ts

@@ -98,6 +98,7 @@ export default function usePhotography() {
     async function getPhotoRecords(params?: {}) {
       if (loading.value) return;
       loading.value = true;
+      console.log('params' , params)
       clientStore.ipc.send(icpList.takePhoto.getPhotoRecords, {
         ...params,
         page: 1,

+ 196 - 65
frontend/src/views/Tpl/Edit/index.vue

@@ -2,82 +2,213 @@
   <headerBar title="编辑模板" />
 
   <marketingEdit
-      v-model:data="data"
-      v-model:index="index"
-      :goods_text="[
-          {
-            key: '设计理念',
-            value: '经典凹出兼具动感同时带来轻盈\n步调轻软,松弛自在蔓延\n立体质感让朝气肆意绽放'
-          },
-          {
-            key: '标题',
-            value: '休闲运动'
-          },
-          {
-            key: '帮面',
-            value: '网布+合成革'
-          },
-          {
-            key: '鞋底',
-            value: '橡胶底'
-          },
-          {
-            key: '颜色',
-            value: '黑色'
-          }
-      ]"
-      :goods_images="[
-          {
-            key: '俯视',
-            value: 'C:\\Users\\Administrator\\Desktop\\img\\A596351\\1.png'
-          },
-          {
-            key: '侧视',
-            value: 'C:\\Users\\Administrator\\Desktop\\img\\A596351\\2.png'
-          },
-          {
-            key: '后跟',
-            value: 'C:\\Users\\Administrator\\Desktop\\img\\A596351\\3.png'
-          },
-          {
-            key: '鞋底',
-            value: 'C:\\Users\\Administrator\\Desktop\\img\\A596351\\4.png'
-          },
-          {
-            key: '内里',
-            value: 'C:\\Users\\Administrator\\Desktop\\img\\A596351\\5.png'
-          }
-      ]"
-      @save="save"
+    v-model:data="data"
+    v-model:index="index"
+    :goods_text="defaultGoodsText"
+    :goods_images="goodsImages"
+    @save="save"
   />
 
-
+  <el-dialog
+      v-model="showNameDialog"
+      title="输入模板名称"
+      :close-on-click-modal="false"
+      :close-on-press-escape="false"
+      width="400px"
+  >
+    <el-input
+        v-model="templateName"
+        placeholder="请输入模板名称"
+        maxlength="50"
+        show-word-limit
+    />
+    <template #footer>
+      <el-button @click="showNameDialog = false">取消</el-button>
+      <el-button type="primary" @click="confirmName">确定</el-button>
+    </template>
+  </el-dialog>
 
 </template>
 
 <script setup lang="ts">
-import { ref, reactive } from 'vue';
+import { computed, ref } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import { ElMessage } from 'element-plus';
 import headerBar from "@/components/header-bar/index.vue";
 import marketingEdit from '@/views/components/marketingEdit'
+import useClientStore from '@/stores/modules/client'
+
+import { saveCustomerTemplate } from '@/apis/other'
+
+
+const route = useRoute();
+const router = useRouter();
+const clientStore = useClientStore();
+const isEdit = !!route.params.id;
+
 const data  = ref([])
 const index = ref(0)
+const templateName = ref('')
+const showNameDialog = ref(false) // 由"保存"触发,而不是进入页面就弹
+const pendingPayload = ref<any | null>(null)
+
+// 编辑模式:从路由 query 中还原模板数据和名称
+if (isEdit) {
+  const query: any = route.query || {}
+
+  // 还原画布 JSON
+  const rawJson = query.customer_template_json as string | undefined
+  if (rawJson) {
+    try {
+      const decoded = decodeURIComponent(rawJson)
+      const parsed = JSON.parse(decoded)
+      if (Array.isArray(parsed)) {
+        data.value = parsed
+      } else {
+        data.value = parsed
+      }
+    } catch (e) {
+      console.error('解析 customer_template_json 失败:', e)
+    }
+  }
+
+  // 名称直接取 template_name,取不到则为空
+  templateName.value = (query.template_name || '') as string
+}
+
+// 默认商品文案
+const defaultGoodsText = [
+  {
+    key: '设计理念',
+    value: '经典凹出兼具动感同时带来轻盈\n步调轻软,松弛自在蔓延\n立体质感让朝气肆意绽放'
+  },
+  {
+    key: '标题',
+    value: '休闲运动'
+  },
+  {
+    key: '帮面',
+    value: '网布+合成革'
+  },
+  {
+    key: '鞋底',
+    value: '橡胶底'
+  },
+  {
+    key: '颜色',
+    value: '黑色'
+  }
+]
+
+// 从路由 query 中解析 goods_images,如果没有就使用默认示例
+const defaultGoodsImages = [
+  {
+    key: '俯视',
+    value: 'C:\\Users\\Administrator\\Desktop\\img\\A596351\\1.png'
+  },
+  {
+    key: '侧视',
+    value: 'C:\\Users\\Administrator\\Desktop\\img\\A596351\\2.png'
+  },
+  {
+    key: '后跟',
+    value: 'C:\\Users\\Administrator\\Desktop\\img\\A596351\\3.png'
+  },
+  {
+    key: '鞋底',
+    value: 'C:\\Users\\Administrator\\Desktop\\img\\A596351\\4.png'
+  },
+  {
+    key: '内里',
+    value: 'C:\\Users\\Administrator\\Desktop\\img\\A596351\\5.png'
+  }
+]
+
+const goodsImages = computed(() => {
+  const raw = route.query.customer_template_images as string | undefined
+  if (!raw) return defaultGoodsImages
+
+  try {
+    const decoded = decodeURIComponent(raw)
+    const parsed = JSON.parse(decoded)
+    if (Array.isArray(parsed) && parsed.length > 0) {
+      return parsed
+    }
+    return defaultGoodsImages
+  } catch (e) {
+    console.error('解析 customer_template_images 失败:', e)
+    return defaultGoodsImages
+  }
+})
+
+
+
+const doSave = async (payload: any) => {
+  try {
+    // 组装请求参数
+    const requestData: any = {
+      customer_template_json: payload, // 所有画布的 JSON
+    }
+    // 编辑时必填 id
+    if (isEdit) {
+      requestData.id = route.params.id
+      requestData.template_name = templateName.value?.trim() || ''
+
+      // 编辑时使用原来的模板名称(不传 template_name,让后端保持原值)
+    } else {
+      // 新增时必填模板名称
+      requestData.template_name = templateName.value?.trim() || ''
+    }
+    // 封面图:取第一张画布的 preview
+    if (payload && payload.length > 0 && payload[0].preview) {
+      requestData.template_cover_image = payload[0].preview
+    } 
+    requestData.customer_template_images = goodsImages.value
+    requestData.template_image_order =  route.query.template_image_order as string | undefined
+    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)
+  }
+}
+
+const confirmName = () => {
+  if (!templateName.value || !templateName.value.trim()) {
+    // 简单校验,保持弹窗打开
+    return
+  }
+  showNameDialog.value = false
+  if (!isEdit && pendingPayload.value) {
+    doSave(pendingPayload.value)
+    pendingPayload.value = null
+  }
+}
+
+const save = (payload: any) => {
+  if (isEdit) {
+    // 编辑模式直接保存,不弹名称框
+    doSave(payload)
+    return
+  }
+
+  // 新增模式下,第一次点击“保存”且没填名称时,先弹输入名称框
+  if (!templateName.value || !templateName.value.trim()) {
+    pendingPayload.value = payload
+    showNameDialog.value = true
+    return
+  }
 
-/*
-* [
-*  {
-*     tpl_url:"",
-*     image_path:"",
-*     name:"",
-*     width:"",
-*     height:"",
-*     bg_color:"",
-*  }
-* ]
-* */
-
-
-const save = (data: any) => {
-  console.log(data)
+  doSave(payload)
 }
 </script>
 

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

+ 9 - 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'
@@ -125,7 +126,7 @@ export default {
   methods: {
     loadTemplate(){
       if(this.hasLoadedTpl) return
-      const tplData = JSON.parse(JSON.stringify(canvas)).map(item => ({
+      const tplData = JSON.parse(JSON.stringify(this.data)).map(item => ({
         canvas_type: item.canvas_type || 'normal',
         canvas_json: (item.canvas_type === 'model' || item.canvas_type === 'scene') ? (item.canvas_type) : (item.canvas_json || ''),
         ...item,
@@ -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){
@@ -532,7 +534,12 @@ export default {
     },
     async createImg(){
        // 1. 组装数据源(后续可以从接口/其他页面传入)
-       const goodsData = [JSON.parse(JSON.stringify(goods)), JSON.parse(JSON.stringify(goods1)), JSON.parse(JSON.stringify(goods2))]
+       const goodsData = [
+        JSON.parse(JSON.stringify(goods)), 
+        // JSON.parse(JSON.stringify(goods1)), 
+        // JSON.parse(JSON.stringify(goods2)),
+        JSON.parse(JSON.stringify(goods4))]
+        
        const canvasJson = JSON.parse(JSON.stringify(canvas))
 
        // 2. 针对每个款号,生成所有画布图片 + 组合长图(在内存中生成 dataURL)

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

@@ -68,8 +68,8 @@ export  default function tpl(){
             <el-button class="mar-left-10"  v-if="!isEmpty"  @click="handleAdjustCanvas">调整画布</el-button>
             <el-button @click="addCanvas"  class="mar-left-10">新增画布</el-button>
             <el-button type="primary" @click="handleSave">保存</el-button>
-            <el-button type="primary" @click="createImg">生成图片</el-button>
-             <el-button class="mar-left-10" @click="handleBack">返回</el-button>
+             <!--  <el-button type="primary" @click="createImg">生成图片</el-button> -->
+            <el-button class="mar-left-10" @click="handleBack">返回</el-button>
            </div>
            
             <!-- 新增:调整画布尺寸弹窗 -->
@@ -85,7 +85,7 @@ export  default function tpl(){
                     <el-radio label="model">模特图</el-radio>
                     <el-radio label="scene">场景图</el-radio>
                   </el-radio-group>
-                  <div style="font-size: 12px; color: #999; margin-top: 8px; line-height: 1.6;">
+                  <div style="margin-left: 20px; font-size: 12px; color: #B0B0B0; margin-top: 0px; line-height: 1.6;">
                     <div v-if="canvasForm.canvas_type === 'normal'">普通画布,可编辑内容</div>
                     <div v-else-if="canvasForm.canvas_type === 'model'">模特图占位,用于前台/后端生成时替换</div>
                     <div v-else-if="canvasForm.canvas_type === 'scene'">场景图占位,用于前台/后端生成时替换</div>

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