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