|
|
@@ -0,0 +1,328 @@
|
|
|
+import fabric from '../PictureEditor/js/fabric-adapter'
|
|
|
+import { buildRenderPlans, normalizeGoods } from './generateImagesPlan'
|
|
|
+
|
|
|
+/**
|
|
|
+ * 根据渲染计划和拍平后的货号数据,真正生成图片(dataURL),按 canvasIndex & imageIndex 返回。
|
|
|
+ *
|
|
|
+ * 注意:
|
|
|
+ * - 不做任何上传,只返回 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,imageIndex:number,dataUrl:string,skuIndexes:number[],skus:any[]}>>}
|
|
|
+ */
|
|
|
+export async function renderImagesByPlans(plans, canvasList, skus) {
|
|
|
+ const results = []
|
|
|
+
|
|
|
+ // 工具:从 canvas_json 创建一个离屏 fabric.Canvas,并应用一次性的渲染逻辑
|
|
|
+ const renderOne = (canvasItem, planForCanvas, imagePlan) =>
|
|
|
+ new Promise((resolve) => {
|
|
|
+ if (!canvasItem || !canvasItem.canvas_json) {
|
|
|
+ return resolve(null)
|
|
|
+ }
|
|
|
+
|
|
|
+ const { canvasIndex } = planForCanvas
|
|
|
+ const { imageIndex, skuIndexes } = imagePlan
|
|
|
+
|
|
|
+ // 解析 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: 'png',
|
|
|
+ enableRetinaScaling: true,
|
|
|
+ })
|
|
|
+
|
|
|
+ fcanvas.dispose()
|
|
|
+
|
|
|
+ resolve({
|
|
|
+ canvasIndex,
|
|
|
+ imageIndex,
|
|
|
+ dataUrl,
|
|
|
+ skuIndexes,
|
|
|
+ skus: usedSkus,
|
|
|
+ })
|
|
|
+ } 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
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 针对每个款号(style),按画布生成所有图片,并额外生成「所有画布组合在一起」的一张长图。
|
|
|
+ *
|
|
|
+ * @param {Array} canvasList 画布配置数组
|
|
|
+ * @param {Array} goodsList 原始商品数据数组 [goods, goods1, ...]
|
|
|
+ * @returns {Promise<Array<{
|
|
|
+ * styleKey: string,
|
|
|
+ * styleNo: string,
|
|
|
+ * images: Array<{canvasIndex:number,imageIndex:number,dataUrl:string,skuIndexes:number[],skus:any[]}>,
|
|
|
+ * combined?: { dataUrl: string, width: number, height: number }
|
|
|
+ * }>>}
|
|
|
+ */
|
|
|
+export async function generateAllStyleImageBundles(canvasList, goodsList) {
|
|
|
+ const bundles = []
|
|
|
+
|
|
|
+ // 工具:把若干 dataURL 竖向拼接为一张长图
|
|
|
+ const composeCombinedImage = (images) =>
|
|
|
+ new Promise((resolve) => {
|
|
|
+ if (!images || !images.length) return resolve(null)
|
|
|
+
|
|
|
+ Promise.all(
|
|
|
+ images.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: 'png',
|
|
|
+ enableRetinaScaling: true,
|
|
|
+ })
|
|
|
+ const result = { dataUrl, width: maxWidth, height: totalHeight }
|
|
|
+ canvas.dispose()
|
|
|
+ resolve(result)
|
|
|
+ }).catch(() => 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、imageIndex 排序,方便后续命名和组合
|
|
|
+ images.sort((a, b) => {
|
|
|
+ if (a.canvasIndex !== b.canvasIndex) return a.canvasIndex - b.canvasIndex
|
|
|
+ return a.imageIndex - b.imageIndex
|
|
|
+ })
|
|
|
+
|
|
|
+ // 组合所有画布图片为一张长图(可以根据需要选择只取每个画布的第一张等)
|
|
|
+ const combined = await composeCombinedImage(images)
|
|
|
+
|
|
|
+ bundles.push({
|
|
|
+ styleKey,
|
|
|
+ styleNo,
|
|
|
+ images,
|
|
|
+ combined,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return bundles
|
|
|
+}
|
|
|
+
|
|
|
+
|