index.vue 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. <script>
  2. import fabric from '../PictureEditor/js/fabric-adapter'
  3. import tpl from './tpl/index'
  4. import viewMixins from '@/views/components/PictureEditor/mixin/view/index'
  5. import actionsMixins from '@/views/components/PictureEditor/mixin/actions/index'
  6. import layerMixins from '@/views/components/PictureEditor/mixin/layer/index'
  7. import colorMixins from '@/views/components/PictureEditor/mixin/color/index'
  8. import editMixins from '@/views/components/PictureEditor/mixin/edit/index'
  9. import { uploadBaseImg } from '@/apis/other'
  10. const FIXED_CANVAS_WIDTH = 800
  11. export default {
  12. name: 'marketingImageEditor',
  13. mixins: [ viewMixins,actionsMixins,layerMixins,colorMixins,editMixins],
  14. props: {
  15. data: {
  16. type: Array,
  17. default:()=>{
  18. return []
  19. }
  20. },
  21. index:{
  22. type: Number,
  23. default: 0
  24. },
  25. goods_text:{
  26. type: Array,
  27. default: ()=>{
  28. return []
  29. }
  30. },
  31. goods_images:{
  32. type: Array,
  33. default: ()=>{
  34. return []
  35. }
  36. },
  37. },
  38. beforeDestroy() {
  39. this.saveCanvasSnapshot()
  40. this.destroyCanvasInstance()
  41. },
  42. data() {
  43. return {
  44. disabled:false,
  45. fcanvas: null,
  46. fcanvasId: '',
  47. scale: 1,
  48. sceneTplImg:"",//生成的时候记录下来,用户重做
  49. canvasForm:{
  50. type:'add',
  51. width: FIXED_CANVAS_WIDTH,
  52. height: '1024',
  53. color:"#fff",
  54. bg_color:'#fff',
  55. visible:false,
  56. }
  57. }
  58. },
  59. template: tpl(),
  60. computed: {
  61. isEmpty(){
  62. return this.data.length === 0
  63. },
  64. this_canvas(){
  65. return this.data[this.index]
  66. }
  67. },
  68. watch: {
  69. index(newValue, oldValue) {
  70. if (this.isEmpty) return
  71. this.saveCanvasSnapshot(oldValue)
  72. this.$nextTick(() => {
  73. this.init()
  74. })
  75. }
  76. },
  77. mounted() {
  78. this.init()
  79. },
  80. activated(){
  81. },
  82. deactivated() {
  83. },
  84. methods: {
  85. // 初始化
  86. async init() {
  87. //不存在数据
  88. if(this.isEmpty ){
  89. this.addCanvas()
  90. return;
  91. }
  92. /*
  93. //画布下不存在模板OSS地址
  94. if(!this.this_canvas){
  95. this.$emit('update:index', 0)
  96. return;
  97. }
  98. if(this.this_canvas.tpl_url){
  99. return;
  100. }*/
  101. this.$emit('canvasStyle:update',{
  102. width: this.this_canvas.width,
  103. height: this.this_canvas.height
  104. })
  105. await this.viewInit()
  106. await this.$nextTick()
  107. const canvasId = `marketing-canvas-${this.index}`
  108. const canvasEl = document.getElementById(canvasId)
  109. if (!canvasEl) return
  110. this.destroyCanvasInstance()
  111. this.fcanvas = new fabric.Canvas(canvasId, {
  112. backgroundColor: this.this_canvas.bg_color,
  113. containerClass:"fcanvas",
  114. // 元素对象被选中时保持在当前z轴,不会跳到最顶层
  115. preserveObjectStacking:true,
  116. width: this.this_canvas.width,
  117. height: this.this_canvas.height
  118. })
  119. this.fcanvasId = canvasId
  120. const hydrateCanvas = () => {
  121. this.updateCanvasState()
  122. // this.minimapInit()
  123. this.actionInit()
  124. this.layerInit();
  125. this.$nextTick(() => {
  126. this.$emit('init')
  127. })
  128. }
  129. if(this.this_canvas.canvas_json){
  130. const jsonData = typeof this.this_canvas.canvas_json === 'string'
  131. ? this.this_canvas.canvas_json
  132. : JSON.stringify(this.this_canvas.canvas_json)
  133. this.fcanvas.loadFromJSON(jsonData, () => {
  134. this.fcanvas.renderAll()
  135. hydrateCanvas()
  136. })
  137. }else{
  138. hydrateCanvas()
  139. }
  140. },
  141. addCanvas(){
  142. this.canvasForm.type = 'add'
  143. this.canvasForm.name = '画布_'+new Date().getTime().toString().substr(8)+Math.round(100)
  144. this.canvasForm.width = FIXED_CANVAS_WIDTH
  145. this.canvasForm.height = 1024
  146. this.canvasForm.bg_color = '#fff'
  147. this.canvasForm.visible = true;
  148. },
  149. handleAdjustCanvas() {
  150. if(!this.this_canvas) return;
  151. this.canvasForm.type = 'edit'
  152. this.canvasForm.name = this.this_canvas.name
  153. this.canvasForm.width = FIXED_CANVAS_WIDTH
  154. this.canvasForm.height = this.this_canvas.height
  155. this.canvasForm.bg_color = this.this_canvas.bg_color
  156. this.canvasForm.visible = true;
  157. },
  158. submitCanvasInfo() {
  159. // 假设 this.canvasForm 包含最新的 width, height 和 color
  160. if(this.canvasForm.type === 'add'){
  161. this.saveCanvasSnapshot()
  162. this.data.push({
  163. tpl_url:"",
  164. image_path:"",
  165. name:this.canvasForm.name,
  166. width:FIXED_CANVAS_WIDTH,
  167. height:this.canvasForm.height,
  168. bg_color:this.canvasForm.bg_color,
  169. canvas_json:'',
  170. })
  171. const nextIndex = this.data.length - 1
  172. this.$emit('update:index',nextIndex)
  173. if(nextIndex === this.index){
  174. this.$nextTick(() => {
  175. this.init()
  176. })
  177. }
  178. /* this.index = this.data.length - 1*/
  179. this.canvasForm.visible = false;
  180. }else{
  181. if(!this.this_canvas){
  182. this.canvasForm.visible = false;
  183. return;
  184. }
  185. const newWidth = FIXED_CANVAS_WIDTH;
  186. const newHeight = this.canvasForm.height;
  187. const newColor = this.canvasForm.bg_color;
  188. // 更新 fcanvas 的宽度和高度
  189. if(this.fcanvas){
  190. if(newWidth !== this.this_canvas.width)this.fcanvas.setWidth(newWidth);
  191. if(newHeight !== this.this_canvas.height)this.fcanvas.setHeight(newHeight);
  192. if(newColor !== this.this_canvas.bg_color)this.fcanvas.setBackgroundColor(newColor);
  193. // 重新渲染以应用更改
  194. this.fcanvas.renderAll();
  195. }
  196. this.data[this.index].name = this.canvasForm.name
  197. this.data[this.index].width = FIXED_CANVAS_WIDTH
  198. this.data[this.index].height = this.canvasForm.height
  199. this.data[this.index].bg_color = this.canvasForm.bg_color
  200. this.canvasForm.visible = false;
  201. }
  202. },
  203. resizeCanvas(width, height) {
  204. // TODO: 实现具体的画布调整逻辑
  205. console.log('调整画布尺寸为:', width, 'x', height);
  206. },
  207. handleSelectCanvas(index){
  208. if(index === this.index) return
  209. this.saveCanvasSnapshot()
  210. this.$emit('update:index', index)
  211. },
  212. canvasBodyStyle(item){
  213. const height = Number(item?.height)
  214. if(!height || Number.isNaN(height)){
  215. return {
  216. minHeight: '200px'
  217. }
  218. }
  219. return {
  220. height: `${height}px`
  221. }
  222. },
  223. saveCanvasSnapshot(targetIndex){
  224. const snapshotIndex = typeof targetIndex === 'number' ? targetIndex : this.index
  225. if(!this.fcanvas || snapshotIndex === undefined || snapshotIndex === null) return
  226. const canvasData = this.data[snapshotIndex]
  227. if(!canvasData) return
  228. const json = JSON.stringify(this.fcanvas.toJSON(['name','sort','mtr','id','selectable','erasable','data-key','data-value']))
  229. if(Object.prototype.hasOwnProperty.call(canvasData, 'canvas_json')){
  230. canvasData.canvas_json = json
  231. }else{
  232. const updated = {
  233. ...canvasData,
  234. canvas_json: json
  235. }
  236. this.data.splice(snapshotIndex, 1, updated)
  237. }
  238. },
  239. destroyCanvasInstance(){
  240. if(!this.fcanvas) return
  241. try{
  242. this.fcanvas.dispose()
  243. }catch(err){
  244. console.warn('[marketingEdit] dispose canvas failed', err)
  245. }finally{
  246. this.fcanvas = null
  247. this.fcanvasId = ''
  248. }
  249. }
  250. }
  251. }
  252. </script>
  253. <style lang="scss" scoped>
  254. @import '@/styles/fcanvas.scss';
  255. .picture-editor-wrap {
  256. position: absolute;
  257. left: 0;
  258. top:0;
  259. right: 0;
  260. bottom:0;
  261. background: #e6e6e6;
  262. overflow: auto;
  263. .picture-editor-wrap_canvas {
  264. position: relative;
  265. margin-top: 85px;
  266. box-sizing: border-box;
  267. .picture-editor-canvas {
  268. min-height: calc(100vh - 85px);
  269. display: flex;
  270. align-items: flex-start;
  271. justify-content: center;
  272. }
  273. }
  274. @import '../PictureEditor/mixin/actions/index.scss';
  275. @import '../PictureEditor/mixin/view/index.scss';
  276. @import '../PictureEditor/mixin/layer/index.scss';
  277. @import '../PictureEditor/mixin/color/index.scss';
  278. @import '../PictureEditor/mixin/edit/index.scss';
  279. @import './tpl/header.scss';
  280. .picture-editor-empty {
  281. top:0;
  282. height: 100%;
  283. background: #fff;
  284. z-index: 10;
  285. }
  286. .add-action-wrap {
  287. position: fixed;
  288. top:80px;
  289. overflow: auto;
  290. left: 0px;
  291. z-index: 10;
  292. bottom: 0;
  293. width: 210px;
  294. background:#fff;
  295. box-shadow: 0px 2px 4px 0px rgba(170,177,255,0.54);
  296. .icon {
  297. margin-right: 5px;
  298. }
  299. .icon-img {
  300. width: 30px;
  301. height: 30px;
  302. border-radius: 5px;
  303. margin-right: 10px;
  304. }
  305. .add-action_title {
  306. border-bottom: 1px solid #eee;
  307. background: #fafafa;
  308. }
  309. .active {
  310. .icon {
  311. border:1px solid $primary2;
  312. }
  313. .text {
  314. color: #000;
  315. }
  316. }
  317. }
  318. }
  319. .canvas-stack {
  320. width: 100%;
  321. display: flex;
  322. flex-direction: column;
  323. gap: 20px;
  324. padding-bottom: 40px;
  325. }
  326. .canvas-stack_item {
  327. width: 100%;
  328. border-radius: 8px;
  329. box-shadow: 0 6px 16px rgba(0,0,0,0.06);
  330. padding: 16px;
  331. box-sizing: border-box;
  332. }
  333. .canvas-stack_header {
  334. display: flex;
  335. justify-content: space-between;
  336. align-items: center;
  337. margin-bottom: 12px;
  338. }
  339. .canvas-stack_title {
  340. display: flex;
  341. gap: 12px;
  342. font-size: 16px;
  343. font-weight: 600;
  344. }
  345. .canvas-stack_title .size {
  346. font-size: 13px;
  347. color: #666;
  348. }
  349. .canvas-stack_body {
  350. background: #fff;
  351. border-radius: 6px;
  352. padding: 12px;
  353. box-sizing: border-box;
  354. }
  355. .canvas-stack_body canvas {
  356. display: block;
  357. margin: 0 auto;
  358. }
  359. .canvas-stack_placeholder {
  360. height: 100%;
  361. display: flex;
  362. align-items: center;
  363. justify-content: center;
  364. flex-direction: column;
  365. color: #888;
  366. font-size: 14px;
  367. text-align: center;
  368. }
  369. .canvas-stack_placeholder img {
  370. max-width: 100%;
  371. border-radius: 4px;
  372. margin-bottom: 8px;
  373. }
  374. .canvas-stack_empty {
  375. width: 100%;
  376. text-align: center;
  377. padding: 80px 0;
  378. color: #666;
  379. }
  380. .fixed-width-tip {
  381. line-height: 32px;
  382. color: #666;
  383. }
  384. </style>