| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884 |
- import fabric from '../PictureEditor/js/fabric-adapter'
- import { buildRenderPlans, normalizeGoods } from './generateImagesPlan'
- /**
- * 根据渲染计划和拍平后的货号数据,真正生成图片(dataURL),按 canvasIndex 返回。
- *
- * 注意:
- * - 不做任何上传,只返回 dataURL,方便在 EXE 或其他环境里落地到本地文件。
- * - 需要在外部保证 canvasList / plans / skus 的来源一致。
- *
- * @param {Array} plans 由 buildRenderPlans 生成的渲染计划
- * @param {Array} canvasList 画布配置数组(包含 canvas_json / width / height / bg_color 等)
- * @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) {
- // 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') {
- // imgPlaceholders.forEach((obj, idx) => {
- // const slotIndex = idx % perCanvasSlots
- // const sku = usedSkus[slotIndex]
- // const angleKey = obj['data-key']
- // if (!sku) {
- // obj.visible = false
- // return
- // }
- // 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 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({
- // format: 'jpeg',
- // multiplier: exportMultiplier,
- // enableRetinaScaling: true,
- // })
- // fcanvas.dispose()
- // resolve({
- // canvasIndex,
- // dataUrl,
- // width: finalWidth,
- // height: finalHeight,
- // })
- // } catch (e) {
- // try {
- // fcanvas.dispose()
- // } catch (e2) {}
- // resolve(null)
- // }
- // })
- // } catch (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) {
- const results = []
- // 工具函数:只检查并压缩图片,返回压缩后的图片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) {
- 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) {
- 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))
-
- // 获取画布对象
- const objs = (json && Array.isArray(json.objects)) ? json.objects : []
- // 1) 处理图片占位(data-type = img)
- const imgPlaceholders = objs.filter((o) => o && o['data-type'] === 'img')
-
- // 为每个图片占位生成压缩后的URL
- const imageUrlMap = new Map()
-
- // 收集所有需要压缩的图片URL
- const urlsToCompress = []
-
- if (mode === 'multiple') {
- imgPlaceholders.forEach((obj, idx) => {
- const slotIndex = idx % perCanvasSlots
- const sku = usedSkus[slotIndex]
- const angleKey = obj['data-key']
-
- if (!sku) {
- return
- }
-
- const url = (sku.pics && sku.pics[angleKey]) || ''
- if (!url) {
- return
- }
-
- urlsToCompress.push({ url, objId: `${idx}_${angleKey}`, originalObj: obj, angleKey, skuIndex: slotIndex })
- })
- } else {
- // 默认 / single:一个货号多角度,一张图只用一个货号
- const sku = usedSkus[0]
- if (!sku) {
- }
-
- 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) {
- obj.visible = false
- return
- }
-
- const objId = `${idx}_${angleKey}`
- const compressedUrl = imageUrlMap.get(objId)
-
- 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
- return
- }
-
- const angleKey = obj['data-key']
- const objId = `${idx}_${angleKey}`
- const compressedUrl = imageUrlMap.get(objId)
-
- 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
- }
- })
- }
- // 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,
- })
- // 添加fabric事件监听器来调试图片加载
- let loadedImageCount = 0
- const totalImagePlaceholders = imgPlaceholders.filter(obj => obj.visible !== false).length
-
- // 重要:使用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, () => {
-
- try {
-
- // 检查所有加载的对象
- const allObjects = fcanvas.getObjects()
-
- // 检查图片对象
- const imageObjects = allObjects.filter(obj => obj.type === 'image')
-
-
- // 确保所有图片对象都正确渲染
- fcanvas.getObjects().forEach(obj => {
- if (obj.type === 'image') {
- // 如果图片有clipPath,确保它正确应用
- if (obj.clipPath) {
- obj.setCoords()
- }
- // 确保图片在canvas边界内正确显示
- obj.setCoords()
- }
- })
-
- 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({
- format: 'jpeg',
- multiplier: exportMultiplier,
- enableRetinaScaling: true,
- })
-
- fcanvas.dispose()
- resolve({
- canvasIndex,
- dataUrl,
- width: finalWidth,
- height: finalHeight,
- })
- } catch (e) {
- try {
- fcanvas.dispose()
- } catch (e2) {
- }
- resolve(null)
- }
- }, reviver)
-
- // 添加fabric错误处理
- fcanvas.on('object:added', (e) => {
- })
-
- } catch (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)
- } else {
- }
- }
- }
- return results
- }
- /**
- * 针对每个款号(style),按画布生成所有图片,并额外生成「所有画布组合在一起」的一张长图。
- *
- * @param {Array} canvasList 画布配置数组
- * @param {Array} goodsList 原始商品数据数组 [goods, goods1, ...]
- * @returns {Promise<Array<{
- * styleKey: string,
- * styleNo: string,
- * images: Array<{canvasIndex:number,dataUrl:string}>,
- * combined?: { dataUrl: string, width: number, height: number }
- * }>>}
- */
- export async function generateAllStyleImageBundles(canvasList, goodsList) {
- const bundles = []
- // 工具:把若干 dataURL 竖向拼接为一张长图
- // 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
- })
- )
- ).then(imageInfos => {
- const validInfos = imageInfos.filter(info => info !== null)
- if (!validInfos.length) {
- el.remove()
- return resolve(null)
- }
- 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)
- el.width = maxWidth
- el.height = totalHeight
- ctx.fillStyle = '#fff'
- ctx.fillRect(0, 0, maxWidth, totalHeight)
- 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 || []) {
- if (!group) continue
- for (const [styleKey, entry] of Object.entries(group)) {
- if (!entry || !Array.isArray(entry['货号资料'])) continue
- const styleNo = entry['款号'] || styleKey
- const styleGoodsList = [{ [styleKey]: entry }]
- const plans = buildRenderPlans(canvasList, styleGoodsList)
- const skus = normalizeGoods(styleGoodsList)
- if (!plans.length || !skus.length) continue
- // eslint-disable-next-line no-await-in-loop
- const images = await renderImagesByPlans(plans, canvasList, skus)
- if (!images.length) continue
- // 按 canvasIndex 排序,方便后续命名和组合
- images.sort((a, b) => a.canvasIndex - b.canvasIndex)
- // 组合所有画布图片为一张长图(可以根据需要选择只取每个画布的第一张等)
- const combined = await composeCombinedImage(images)
- bundles.push({
- styleKey,
- styleNo,
- images,
- combined,
- })
- }
- }
- 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 }
- }
|