| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330 |
- 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: 'jpeg',
- multiplier:2,
- 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: 'jpeg',
- multiplier:2,
- 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
- }
|