generateImagesRender.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. import fabric from '../PictureEditor/js/fabric-adapter'
  2. import { buildRenderPlans, normalizeGoods } from './generateImagesPlan'
  3. /**
  4. * 根据渲染计划和拍平后的货号数据,真正生成图片(dataURL),按 canvasIndex & imageIndex 返回。
  5. *
  6. * 注意:
  7. * - 不做任何上传,只返回 dataURL,方便在 EXE 或其他环境里落地到本地文件。
  8. * - 需要在外部保证 canvasList / plans / skus 的来源一致。
  9. *
  10. * @param {Array} plans 由 buildRenderPlans 生成的渲染计划
  11. * @param {Array} canvasList 画布配置数组(包含 canvas_json / width / height / bg_color 等)
  12. * @param {Array} skus normalizeGoods(goodsList) 的结果
  13. * @returns {Promise<Array<{canvasIndex:number,imageIndex:number,dataUrl:string,skuIndexes:number[],skus:any[]}>>}
  14. */
  15. export async function renderImagesByPlans(plans, canvasList, skus) {
  16. const results = []
  17. // 工具:从 canvas_json 创建一个离屏 fabric.Canvas,并应用一次性的渲染逻辑
  18. const renderOne = (canvasItem, planForCanvas, imagePlan) =>
  19. new Promise((resolve) => {
  20. if (!canvasItem || !canvasItem.canvas_json) {
  21. return resolve(null)
  22. }
  23. const { canvasIndex } = planForCanvas
  24. const { imageIndex, skuIndexes } = imagePlan
  25. // 解析 JSON 为可修改的对象
  26. let json
  27. try {
  28. json = typeof canvasItem.canvas_json === 'string'
  29. ? JSON.parse(canvasItem.canvas_json)
  30. : JSON.parse(JSON.stringify(canvasItem.canvas_json))
  31. } catch (e) {
  32. console.warn('[generateImagesRender] parse canvas_json failed', e)
  33. return resolve(null)
  34. }
  35. const width = Number(canvasItem.width) || 395
  36. const height = Number(canvasItem.height) || 600
  37. const bgColor = canvasItem.bg_color || '#fff'
  38. try {
  39. const mode = canvasItem.multi_goods_mode || ''
  40. const perCanvasSlots = planForCanvas.perCanvasSlots || 1
  41. const usedSkus = (skuIndexes || []).map((idx) => (idx != null ? skus[idx] : null))
  42. // 先在原始 JSON 上做数据替换,避免 setSrc 的异步问题;
  43. // loadFromJSON 会在所有图片加载完成后才触发回调。
  44. const objs = (json && Array.isArray(json.objects)) ? json.objects : []
  45. // 1) 处理图片占位(data-type = img)
  46. const imgPlaceholders = objs.filter((o) => o && o['data-type'] === 'img')
  47. if (mode === 'multiple') {
  48. // 多个货号同角度:同一画布中,有多个 img 占位,按顺序对应 skuIndexes
  49. imgPlaceholders.forEach((obj, idx) => {
  50. const slotIndex = idx % perCanvasSlots
  51. const sku = usedSkus[slotIndex]
  52. if (!sku) {
  53. obj.visible = false
  54. return
  55. }
  56. const angleKey = obj['data-key']
  57. const url = (sku.pics && sku.pics[angleKey]) || ''
  58. if (!url) {
  59. obj.visible = false
  60. return
  61. }
  62. obj.visible = true
  63. obj['data-value'] = url
  64. obj.src = url
  65. })
  66. } else {
  67. // 默认 / single:一个货号多角度,一张图只用一个货号
  68. const sku = usedSkus[0]
  69. imgPlaceholders.forEach((obj) => {
  70. if (!sku) {
  71. obj.visible = false
  72. return
  73. }
  74. const angleKey = obj['data-key']
  75. const url = (sku.pics && sku.pics[angleKey]) || ''
  76. if (!url) {
  77. obj.visible = false
  78. return
  79. }
  80. obj.visible = true
  81. obj['data-value'] = url
  82. obj.src = url
  83. })
  84. }
  85. // 2) 处理文字占位(data-type = text)
  86. const textPlaceholders = objs.filter((o) => o && o['data-type'] === 'text')
  87. if (textPlaceholders.length) {
  88. // 通用的 key -> 文本 映射函数,
  89. const mapKeyToText = (sku, key, defaultVal) => {
  90. if (!sku) return defaultVal
  91. let textVal = defaultVal || ''
  92. if (key === '颜色') {
  93. textVal = sku.color || textVal
  94. } else if (key === '货号') {
  95. textVal = sku.sku || textVal
  96. }
  97. // 兜底:去 raw 里找同名字段(支持 卖点 / 使用场景 / 其他自定义字段)
  98. if ((!textVal || textVal === defaultVal) && sku.raw && sku.raw[key] != null) {
  99. textVal = sku.raw[key]
  100. }
  101. // 再兜底:如果 sku 上有同名字段
  102. if ((!textVal || textVal === defaultVal) && sku[key] != null) {
  103. textVal = sku[key]
  104. }
  105. return textVal
  106. }
  107. if (mode === 'multiple') {
  108. // 多个货号同角度:
  109. // - 按 data-key + 出现顺序把文字分配给不同货号
  110. // - 如果该 slot 没有对应货号(usedSkus[slotIndex] 为 null),则隐藏该文字层
  111. const keyCounter = {}
  112. textPlaceholders.forEach((obj) => {
  113. const key = obj['data-key']
  114. if (!key) return
  115. const idxForKey = keyCounter[key] || 0
  116. keyCounter[key] = idxForKey + 1
  117. const slotIndex = idxForKey % perCanvasSlots
  118. const sku = usedSkus[slotIndex]
  119. if (!sku) {
  120. obj.visible = false
  121. return
  122. }
  123. const origin = obj['data-value'] || ''
  124. const textVal = mapKeyToText(sku, key, origin)
  125. obj.visible = true
  126. obj.text = textVal
  127. obj['data-value'] = textVal
  128. })
  129. } else {
  130. // 默认 / single:全部文字都使用同一个货号(默认模式只生成 1 张,用第一个货号)
  131. const sku = usedSkus[0]
  132. if (sku) {
  133. textPlaceholders.forEach((obj) => {
  134. const key = obj['data-key']
  135. if (!key) return
  136. const origin = obj['data-value'] || ''
  137. const textVal = mapKeyToText(sku, key, origin)
  138. obj.visible = true
  139. obj.text = textVal
  140. obj['data-value'] = textVal
  141. })
  142. }
  143. }
  144. }
  145. // 创建离屏 canvas
  146. const el = document.createElement('canvas')
  147. el.width = width
  148. el.height = height
  149. const fcanvas = new fabric.Canvas(el, {
  150. backgroundColor: bgColor,
  151. width,
  152. height,
  153. renderOnAddRemove: false,
  154. })
  155. // 把已经替换好动态数据的 JSON 加载进 fabric;
  156. // loadFromJSON 会等所有图片资源加载完之后才调用回调函数
  157. fcanvas.loadFromJSON(json, () => {
  158. try {
  159. fcanvas.renderAll()
  160. const dataUrl = fcanvas.toDataURL({
  161. format: 'jpeg',
  162. multiplier:2,
  163. enableRetinaScaling: true,
  164. })
  165. fcanvas.dispose()
  166. resolve({
  167. canvasIndex,
  168. imageIndex,
  169. dataUrl,
  170. skuIndexes,
  171. skus: usedSkus,
  172. })
  173. } catch (e) {
  174. console.warn('[generateImagesRender] render one failed in callback', e)
  175. try {
  176. fcanvas.dispose()
  177. } catch (e2) {}
  178. resolve(null)
  179. }
  180. })
  181. } catch (e) {
  182. console.warn('[generateImagesRender] render one failed', e)
  183. resolve(null)
  184. }
  185. })
  186. for (const plan of plans || []) {
  187. const canvasItem = canvasList[plan.canvasIndex]
  188. if (!canvasItem) continue
  189. for (const imgPlan of plan.images || []) {
  190. // eslint-disable-next-line no-await-in-loop
  191. const res = await renderOne(canvasItem, plan, imgPlan)
  192. if (res) results.push(res)
  193. }
  194. }
  195. return results
  196. }
  197. /**
  198. * 针对每个款号(style),按画布生成所有图片,并额外生成「所有画布组合在一起」的一张长图。
  199. *
  200. * @param {Array} canvasList 画布配置数组
  201. * @param {Array} goodsList 原始商品数据数组 [goods, goods1, ...]
  202. * @returns {Promise<Array<{
  203. * styleKey: string,
  204. * styleNo: string,
  205. * images: Array<{canvasIndex:number,imageIndex:number,dataUrl:string,skuIndexes:number[],skus:any[]}>,
  206. * combined?: { dataUrl: string, width: number, height: number }
  207. * }>>}
  208. */
  209. export async function generateAllStyleImageBundles(canvasList, goodsList) {
  210. const bundles = []
  211. // 工具:把若干 dataURL 竖向拼接为一张长图
  212. const composeCombinedImage = (images) =>
  213. new Promise((resolve) => {
  214. if (!images || !images.length) return resolve(null)
  215. Promise.all(
  216. images.map(img =>
  217. new Promise(res => {
  218. fabric.Image.fromURL(
  219. img.dataUrl,
  220. (oImg) => res(oImg),
  221. { crossOrigin: 'anonymous' }
  222. )
  223. })
  224. )
  225. ).then(fabricImages => {
  226. const widths = fabricImages.map(i => i.width * (i.scaleX || 1))
  227. const heights = fabricImages.map(i => i.height * (i.scaleY || 1))
  228. const totalHeight = heights.reduce((a, b) => a + b, 0)
  229. const maxWidth = widths.reduce((a, b) => Math.max(a, b), 0)
  230. const el = document.createElement('canvas')
  231. el.width = maxWidth
  232. el.height = totalHeight
  233. const canvas = new fabric.Canvas(el, {
  234. backgroundColor: '#fff',
  235. width: maxWidth,
  236. height: totalHeight,
  237. renderOnAddRemove: false,
  238. })
  239. let currentTop = 0
  240. fabricImages.forEach((img, idx) => {
  241. const w = widths[idx]
  242. const h = heights[idx]
  243. img.set({
  244. left: (maxWidth - w) / 2,
  245. top: currentTop,
  246. })
  247. currentTop += h
  248. canvas.add(img)
  249. })
  250. canvas.renderAll()
  251. const dataUrl = canvas.toDataURL({
  252. format: 'jpeg',
  253. multiplier:2,
  254. enableRetinaScaling: true,
  255. })
  256. const result = { dataUrl, width: maxWidth, height: totalHeight }
  257. canvas.dispose()
  258. resolve(result)
  259. }).catch(() => resolve(null))
  260. })
  261. // 按款号分组处理,避免不同款号的货号混在一起
  262. for (const group of goodsList || []) {
  263. if (!group) continue
  264. for (const [styleKey, entry] of Object.entries(group)) {
  265. if (!entry || !Array.isArray(entry['货号资料'])) continue
  266. const styleNo = entry['款号'] || styleKey
  267. const styleGoodsList = [{ [styleKey]: entry }]
  268. const plans = buildRenderPlans(canvasList, styleGoodsList)
  269. const skus = normalizeGoods(styleGoodsList)
  270. if (!plans.length || !skus.length) continue
  271. // eslint-disable-next-line no-await-in-loop
  272. const images = await renderImagesByPlans(plans, canvasList, skus)
  273. if (!images.length) continue
  274. // 按 canvasIndex、imageIndex 排序,方便后续命名和组合
  275. images.sort((a, b) => {
  276. if (a.canvasIndex !== b.canvasIndex) return a.canvasIndex - b.canvasIndex
  277. return a.imageIndex - b.imageIndex
  278. })
  279. // 组合所有画布图片为一张长图(可以根据需要选择只取每个画布的第一张等)
  280. const combined = await composeCombinedImage(images)
  281. bundles.push({
  282. styleKey,
  283. styleNo,
  284. images,
  285. combined,
  286. })
  287. }
  288. }
  289. return bundles
  290. }