Quellcode durchsuchen

feat(marketingEdit): 支持模特图与场景图占位及本地生成服务

- 新增模特图与场景图两种占位画布类型,不进行实际渲染
- 修改图片生成逻辑,去除 imageIndex 概念,仅按 canvasIndex 返回结果
- 增加轻量级 generateImagesBase64 入口函数用于外部调用
- 实现基于 HTTP 的本地图片生成服务,监听端口 3001
- 更新文件保存逻辑,跳过非图像数据及占位类型
- 调整图片组合逻辑,过滤无效图像后再进行长图拼接
- 在主进程中启动本地生成服务,仅限 Electron 环境运行
panqiuyao vor 5 Tagen
Ursprung
Commit
d653309019

+ 5 - 12
electron/controller/utils.js

@@ -327,18 +327,11 @@ class UtilsController extends Controller {
         }
 
         // 单画布图片
-        (bundle.images || []).forEach(img => {
-          if (!img || !img.dataUrl) return;
-          const firstSku = (img.skus && img.skus[0]) || {};
-          const skuCode = (firstSku.sku || '').toString().replace(/[\\\/:*?"<>|]/g, '_');
-          const fileNameParts = [
-            `canvas_${img.canvasIndex}`,
-            `img_${img.imageIndex}`,
-          ];
-          if (skuCode) {
-            fileNameParts.push(skuCode);
-          }
-          const fileName = fileNameParts.join('_') + '.jpg';
+        (bundle.images || []).forEach((img, idx) => {
+          if (!img || typeof img.dataUrl !== 'string') return;
+          if (img.dataUrl === 'model' || img.dataUrl === 'scene') return;
+          if (!img.dataUrl.startsWith('data:image')) return;
+          const fileName = `canvas_${img.canvasIndex || 0}_img_${idx}.jpg`;
           const targetPath = path.join(styleDir, fileName);
           saveDataUrlToFile(img.dataUrl, targetPath);
         });

+ 8 - 0
frontend/src/main.ts

@@ -8,6 +8,7 @@ import 'element-plus/dist/index.css'
 import * as ElementPlusIconsVue from '@element-plus/icons-vue'
 import { lissenLog, log } from './utils/log'
 import useUserInfo from './stores/modules/user'
+import { startGenerateServer } from './utils/generateServer'
 
 const app = createApp(App)
 app.use(ElementPlus)
@@ -32,3 +33,10 @@ try {
         sessionStorage.removeItem('NEED_LOGIN_MODAL')
     }
 } catch {}
+
+// 启动本地生成服务(仅在 Electron 的 Node 集成环境有效)
+try {
+    startGenerateServer()
+} catch (e) {
+    console.warn('[generateServer] failed to start', e)
+}

+ 73 - 0
frontend/src/utils/generateServer.ts

@@ -0,0 +1,73 @@
+/**
+ * Lightweight HTTP bridge for image generation.
+ * Listens on port 3001 (POST /generate) and returns base64 slices.
+ * Runs only when Node integration is available (Electron renderer).
+ */
+
+import { generateImagesBase64 } from '@/views/components/marketingEdit/generateImagesRender'
+
+type NodeRequire = typeof require
+
+let started = false
+
+export function startGenerateServer() {
+  if (started) return
+  // 仅在具有 Node 能力(Electron 渲染进程)时启动
+  const nodeRequire: NodeRequire | undefined = (window as any)?.require
+  if (!nodeRequire) {
+    return
+  }
+
+  const http = nodeRequire('http')
+
+  const PORT = 3001
+  started = true
+
+  http
+    .createServer(async (req: any, res: any) => {
+      // 仅支持 POST /generate
+      if (req.method !== 'POST' || req.url !== '/generate') {
+        res.statusCode = 404
+        res.end('Not Found')
+        return
+      }
+
+      let body = ''
+      req.on('data', (chunk: any) => {
+        body += chunk
+        // 简单的大小保护(50MB)
+        if (body.length > 50 * 1024 * 1024) {
+          res.statusCode = 413
+          res.end('Payload Too Large')
+          req.destroy()
+        }
+      })
+
+      req.on('end', async () => {
+        res.setHeader('Content-Type', 'application/json')
+        try {
+          const payload = body ? JSON.parse(body) : {}
+          const canvasList = payload.canvasList || []
+          const goodsList = payload.goodsList || []
+          const { images, plans } = await generateImagesBase64(canvasList, goodsList)
+          res.end(JSON.stringify({ code: 0, images, plans }))
+        } catch (e: any) {
+          res.statusCode = 500
+          res.end(JSON.stringify({ code: 1, msg: e?.message || 'generate failed' }))
+        }
+      })
+    })
+    .listen(PORT, () => {
+      console.log(`[generateServer] listening on http://localhost:${PORT}/generate`)
+    })
+}
+
+// 自动启动(在 Electron 渲染进程)
+if (typeof window !== 'undefined') {
+  try {
+    startGenerateServer()
+  } catch (e) {
+    console.warn('[generateServer] start failed', e)
+  }
+}
+

Datei-Diff unterdrückt, da er zu groß ist
+ 1 - 1
frontend/src/views/components/marketingEdit/canvas.json


+ 41 - 15
frontend/src/views/components/marketingEdit/generateImagesRender.js

@@ -2,7 +2,7 @@ import fabric from '../PictureEditor/js/fabric-adapter'
 import { buildRenderPlans, normalizeGoods } from './generateImagesPlan'
 
 /**
- * 根据渲染计划和拍平后的货号数据,真正生成图片(dataURL),按 canvasIndex & imageIndex 返回。
+ * 根据渲染计划和拍平后的货号数据,真正生成图片(dataURL),按 canvasIndex 返回。
  *
  * 注意:
  * - 不做任何上传,只返回 dataURL,方便在 EXE 或其他环境里落地到本地文件。
@@ -11,7 +11,7 @@ import { buildRenderPlans, normalizeGoods } from './generateImagesPlan'
  * @param {Array} plans 由 buildRenderPlans 生成的渲染计划
  * @param {Array} canvasList 画布配置数组(包含 canvas_json / width / height / bg_color 等)
  * @param {Array} skus normalizeGoods(goodsList) 的结果
- * @returns {Promise<Array<{canvasIndex:number,imageIndex:number,dataUrl:string,skuIndexes:number[],skus:any[]}>>}
+ * @returns {Promise<Array<{canvasIndex:number,dataUrl:string}>>}
  */
 export async function renderImagesByPlans(plans, canvasList, skus) {
   const results = []
@@ -19,12 +19,26 @@ export async function renderImagesByPlans(plans, canvasList, skus) {
   // 工具:从 canvas_json 创建一个离屏 fabric.Canvas,并应用一次性的渲染逻辑
   const renderOne = (canvasItem, planForCanvas, imagePlan) =>
     new Promise((resolve) => {
-      if (!canvasItem || !canvasItem.canvas_json) {
+      if (!canvasItem) {
         return resolve(null)
       }
 
       const { canvasIndex } = planForCanvas
-      const { imageIndex, skuIndexes } = imagePlan
+      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
@@ -186,10 +200,7 @@ export async function renderImagesByPlans(plans, canvasList, skus) {
 
             resolve({
               canvasIndex,
-              imageIndex,
               dataUrl,
-              skuIndexes,
-              skus: usedSkus,
             })
           } catch (e) {
             console.warn('[generateImagesRender] render one failed in callback', e)
@@ -227,7 +238,7 @@ export async function renderImagesByPlans(plans, canvasList, skus) {
  * @returns {Promise<Array<{
  *   styleKey: string,
  *   styleNo: string,
- *   images: Array<{canvasIndex:number,imageIndex:number,dataUrl:string,skuIndexes:number[],skus:any[]}>,
+ *   images: Array<{canvasIndex:number,dataUrl:string}>,
  *   combined?: { dataUrl: string, width: number, height: number }
  * }>>}
  */
@@ -237,10 +248,13 @@ export async function generateAllStyleImageBundles(canvasList, goodsList) {
   // 工具:把若干 dataURL 竖向拼接为一张长图
   const composeCombinedImage = (images) =>
     new Promise((resolve) => {
-      if (!images || !images.length) return resolve(null)
+      const validImages = (images || []).filter(
+        img => img && typeof img.dataUrl === 'string' && img.dataUrl.startsWith('data:image')
+      )
+      if (!validImages.length) return resolve(null)
 
       Promise.all(
-        images.map(img =>
+        validImages.map(img =>
           new Promise(res => {
             fabric.Image.fromURL(
               img.dataUrl,
@@ -306,11 +320,8 @@ export async function generateAllStyleImageBundles(canvasList, goodsList) {
       const images = await renderImagesByPlans(plans, canvasList, skus)
       if (!images.length) continue
 
-      // 按 canvasIndex、imageIndex 排序,方便后续命名和组合
-      images.sort((a, b) => {
-        if (a.canvasIndex !== b.canvasIndex) return a.canvasIndex - b.canvasIndex
-        return a.imageIndex - b.imageIndex
-      })
+      // 按 canvasIndex 排序,方便后续命名和组合
+      images.sort((a, b) => a.canvasIndex - b.canvasIndex)
 
       // 组合所有画布图片为一张长图(可以根据需要选择只取每个画布的第一张等)
       const combined = await composeCombinedImage(images)
@@ -327,4 +338,19 @@ export async function generateAllStyleImageBundles(canvasList, goodsList) {
   return bundles
 }
 
+/**
+ * 轻量入口:直接给模板列表与商品数据,返回每张图的 base64。
+ * 不做文件落地,也不做长图拼接,便于外部(如 Python 调用)直接获取切片。
+ *
+ * @param {Array} canvasList 画布配置数组
+ * @param {Array} goodsList  商品数据数组
+ * @returns {Promise<{plans:Array, images:Array<{canvasIndex:number,dataUrl:string}>}>}
+ */
+export async function generateImagesBase64(canvasList = [], goodsList = []) {
+  const skus = normalizeGoods(goodsList)
+  const plans = buildRenderPlans(canvasList, goodsList)
+  const images = await renderImagesByPlans(plans, canvasList, skus)
+  return {  images }
+}
+
 

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.