index.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. import Teleport from '@/components/Teleport'
  2. import * as TextboxConfig from './module/TextboxConfig'
  3. import fabric from "../../js/fabric-adapter";
  4. import {markRaw,reactive} from "vue";
  5. // 将默认的 clipSettings 提取为常量
  6. const DEFAULT_CLIP_SETTINGS = {
  7. shape: '',
  8. width: 100,
  9. height: 100,
  10. radius: 50,
  11. offsetX: 50,
  12. offsetY: 50,
  13. showClipStroke: true,
  14. strokeColor: '#000000',
  15. strokeWidth: 1,
  16. rectRadius: 0,
  17. svgUrl: '',
  18. svgWidth: 100,
  19. svgHeight: 100,
  20. };
  21. export default {
  22. components: {
  23. Teleport,
  24. },
  25. data() {
  26. return {
  27. TextboxConfig:TextboxConfig,
  28. options:TextboxConfig.fontFamily,
  29. fontFamilyStyle:"",
  30. opacityValue: 1,
  31. layerState: {
  32. fontSize: 16,
  33. lineHeight: 1.2,
  34. charSpacing: 0,
  35. fill: '#000000',
  36. textAlign: 'left',
  37. },
  38. shadowText:{
  39. x:0,
  40. y:0,
  41. vague:0,
  42. color:'#000',
  43. },
  44. clipSettings: {
  45. ...DEFAULT_CLIP_SETTINGS,
  46. },
  47. strokeObj: null // 用于引用当前描边图形
  48. }
  49. },
  50. props: {
  51. },
  52. computed: {
  53. editLayer(){
  54. let object = {}
  55. if(this.selected && this.selected.length === 1){
  56. object = this.selected[0]
  57. }
  58. // return reactive(object)
  59. return object
  60. },
  61. fontFamily(){
  62. let obj = {}
  63. this.TextboxConfig.fontFamily.map(item=>{
  64. obj[item.name] = item
  65. })
  66. return obj
  67. },
  68. shadow(){
  69. let shadowText = this.shadowText
  70. return `${shadowText.x}px ${shadowText.y}px ${shadowText.vague}px ${shadowText.color} `
  71. }
  72. },
  73. watch: {
  74. editLayer(){
  75. this.fontFamilyStyle = this.editLayer.fontFamily
  76. this.opacityValue = (this.editLayer && typeof this.editLayer.opacity === 'number')
  77. ? this.editLayer.opacity
  78. : 1
  79. this.layerState = {
  80. ...this.layerState,
  81. fontSize: this.editLayer?.fontSize ?? 16,
  82. lineHeight: this.editLayer?.lineHeight ?? 1,
  83. charSpacing: this.editLayer?.charSpacing ?? 0,
  84. fill: this.editLayer?.fill ?? '#000000',
  85. textAlign: this.editLayer?.textAlign ?? 'left',
  86. }
  87. switch (this.editLayer.type){
  88. case "textbox":
  89. if(this.editLayer.shadow){
  90. this.shadowText = {
  91. x:this.editLayer.shadow.offsetX || 0,
  92. y:this.editLayer.shadow.offsetY || 0,
  93. vague:this.editLayer.shadow.blur || 0,
  94. color:this.editLayer.shadow.color || '#000',
  95. }
  96. }else{
  97. this.shadowText = {
  98. x:0,
  99. y:0,
  100. vague:0,
  101. color:'#000'
  102. }
  103. }
  104. break;
  105. case "image":
  106. // 优先使用默认值
  107. let imageSettings = { ...DEFAULT_CLIP_SETTINGS };
  108. // 若已有 clipPath,合并其中的属性
  109. if (this.editLayer.clipPath) {
  110. imageSettings = {
  111. ...imageSettings,
  112. width: this.editLayer.clipPath.width || imageSettings.width,
  113. height: this.editLayer.clipPath.height || imageSettings.height,
  114. offsetX: this.editLayer.clipPath.left || imageSettings.offsetX,
  115. offsetY: this.editLayer.clipPath.top || imageSettings.offsetY,
  116. shape: this.editLayer.clipPath.type || imageSettings.shape,
  117. };
  118. }
  119. // 如果没有剪裁路径,则 shape 为 ""
  120. if (!this.editLayer.clipPath) {
  121. imageSettings.shape = '';
  122. }
  123. // 更新 clipSettings,用于 UI 显示
  124. this.clipSettings = imageSettings;
  125. // 同步描边图形(如果存在)
  126. if (this.editLayer.strokeObj) {
  127. this.fcanvas.add(markRaw(this.editLayer.strokeObj));
  128. }
  129. break;
  130. }
  131. },
  132. selected(){
  133. this.opacityValue = (this.editLayer && typeof this.editLayer.opacity === 'number')
  134. ? this.editLayer.opacity
  135. : 1
  136. this.layerState = {
  137. ...this.layerState,
  138. fontSize: this.editLayer?.fontSize ?? 16,
  139. lineHeight: this.editLayer?.lineHeight ?? 1,
  140. charSpacing: this.editLayer?.charSpacing ?? 0,
  141. fill: this.editLayer?.fill ?? '#000000',
  142. textAlign: this.editLayer?.textAlign ?? 'left',
  143. }
  144. }
  145. },
  146. created() {
  147. },
  148. mounted() {
  149. },
  150. methods:{
  151. judge(){
  152. return this.editLayer?.id
  153. },
  154. //翻转
  155. Flip(type){
  156. if(!this.judge()) return;
  157. switch (type){
  158. case 'X':
  159. this.editLayer.set({
  160. flipX:!this.editLayer.flipX,
  161. })
  162. break;
  163. case 'Y':
  164. this.editLayer.set({
  165. flipY:!this.editLayer.flipY,
  166. })
  167. break;
  168. }
  169. this.updateCanvasState()
  170. this.fcanvas.requestRenderAll()
  171. },
  172. //合并组
  173. setGroup(){
  174. const group = this.fcanvas.getActiveObject().toGroup();
  175. this.fcanvas.requestRenderAll();
  176. this.selected = [group]
  177. if(this.getLayers) this.getLayers()
  178. this.updateCanvasState()
  179. },
  180. //取消组
  181. unGroup(){
  182. if (!this.fcanvas.getActiveObject()) {
  183. return;
  184. }
  185. if (this.fcanvas.getActiveObject().type !== 'group') {
  186. return;
  187. }
  188. this.fcanvas.getActiveObject().toActiveSelection();
  189. this.selected = this.fcanvas.getActiveObject()._objects
  190. this.fcanvas.requestRenderAll();
  191. if(this.getLayers) this.getLayers()
  192. this.updateCanvasState()
  193. },
  194. //编辑文字
  195. editObj(data = {
  196. label:'',
  197. value:'',
  198. action:'set',
  199. }){
  200. if(!this.judge()) return;
  201. let {label,value,action } = data
  202. action = action || 'set'
  203. // 构造待设置的参数;仅当 label 为 fill 且当前无填充时兜底 #000
  204. let params = value
  205. if (label) {
  206. params = { [label]: value }
  207. if (label === 'fill' && !this.editLayer.fill) {
  208. params.fill = value || '#000'
  209. }
  210. }
  211. this.editLayer[action](params)
  212. if (Array.isArray(this.editLayer._objects)){
  213. this.editLayer._objects?.forEach(item=>{
  214. item[action](params)
  215. })
  216. }
  217. // 同步常用文本状态到 layerState,避免 UI 视图不同步
  218. if (['fontSize','lineHeight','charSpacing','fill','textAlign'].includes(label)) {
  219. this.layerState = {
  220. ...this.layerState,
  221. [label]: value,
  222. }
  223. }
  224. this.updateCanvasState()
  225. this.fcanvas.requestRenderAll()
  226. },
  227. // 通用设置图层属性,解决 markRaw 不响应问题
  228. setLayerAttr(name, val){
  229. if(!this.editLayer) return
  230. this.layerState = {
  231. ...this.layerState,
  232. [name]: val,
  233. }
  234. this.editLayer.set(name, val)
  235. if(Array.isArray(this.editLayer._objects)){
  236. this.editLayer._objects.forEach(o => o.set(name, val))
  237. }
  238. this.updateCanvasState()
  239. this.fcanvas && this.fcanvas.requestRenderAll()
  240. },
  241. // 设置透明度(解决 Vue3 markRaw 不响应)
  242. setOpacity(val){
  243. if(!this.editLayer) return
  244. const num = Number(val)
  245. this.opacityValue = num
  246. this.editLayer.set('opacity', num)
  247. if(Array.isArray(this.editLayer._objects)){
  248. this.editLayer._objects.forEach(o => o.set('opacity', num))
  249. }
  250. this.updateCanvasState()
  251. this.fcanvas && this.fcanvas.requestRenderAll()
  252. },
  253. async setFontFamily(val,item=null){
  254. console.log(val)
  255. console.log(item)
  256. console.log(this.fontFamily)
  257. if(!item && !this.judge()) return;
  258. let font = this.fontFamily[val]
  259. console.log(font)
  260. if(!font) return;
  261. await loadFont(font.name,font.src)
  262. let obj = item || this.editLayer
  263. obj.set({'fontFamily': val});
  264. obj.setSelectionStyles({'fontFamily': val});
  265. this.fontFamilyStyle = val
  266. this.updateCanvasState()
  267. await this.$nextTick()
  268. this.fcanvas.requestRenderAll()
  269. async function loadFont(_fontName, _fontUrl) {
  270. return new Promise((resolve, reject) => {
  271. if (checkFont(_fontName)) {
  272. console.log('已有字体:', _fontName)
  273. return resolve(true)
  274. }
  275. let prefont = new FontFace(
  276. _fontName,
  277. 'url(' + _fontUrl + ')'
  278. );
  279. prefont.load().then(function (loaded_face) {
  280. document.fonts.add(loaded_face);
  281. // document.body.style.fontFamily = (_fontFamily ? _fontFamily + ',' : _fontFamily) + _fontName;
  282. console.log('字体加载成功', loaded_face, document.fonts)
  283. return resolve(true)
  284. }).catch(function (error) {
  285. console.log('字体加载失败', error)
  286. return reject(error)
  287. })
  288. })
  289. }
  290. function checkFont(name){
  291. let values = document.fonts.values();
  292. let isHave=false;
  293. let item = values.next();
  294. while(!item.done&&!isHave)
  295. {
  296. let fontFace=item.value;
  297. if(fontFace.family==name)
  298. {
  299. isHave=true;
  300. }
  301. item=values.next();
  302. }
  303. return isHave;
  304. }
  305. },
  306. remoteMethod(query){
  307. if (query !== '') {
  308. this.options = this.TextboxConfig.fontFamily.filter(item => {
  309. return item.localFile.toLowerCase()
  310. .indexOf(query.toLowerCase()) > -1;
  311. });
  312. } else {
  313. this.options = this.TextboxConfig.fontFamily
  314. }
  315. },
  316. async applyClipPath() {
  317. if (!this.judge()) return;
  318. const { shape, width, height, radius, offsetX, offsetY, rectRadius } = this.clipSettings;
  319. if (shape === '') {
  320. // 选择“无”时,直接清除剪裁
  321. this.clearClipPath();
  322. return;
  323. }
  324. try {
  325. // 创建纯剪裁区域(不带描边)
  326. let clipObj;
  327. if (shape === 'svg') {
  328. // 使用 fabric.js 加载远程 SVG
  329. clipObj = await new Promise((resolve, reject) => {
  330. fabric.loadSVGFromURL(
  331. this.clipSettings.svgUrl,
  332. (loadedObjects, options) => {
  333. console.log('=============');
  334. // 创建组合对象
  335. const svgGroup = fabric.util.groupSVGElements(loadedObjects, options);
  336. console.log(svgGroup);
  337. // 设置尺寸
  338. svgGroup.scaleToWidth(this.clipSettings.svgWidth);
  339. console.log(this.clipSettings.svgWidth);
  340. // svgGroup.scaleToHeight(this.clipSettings.svgHeight);
  341. svgGroup.set({
  342. absolutePositioned: true,
  343. left: offsetX,
  344. top: offsetY,
  345. })
  346. resolve(svgGroup);
  347. }
  348. )
  349. });
  350. }else if (shape === 'rect') {
  351. if (width <= 0 || height <= 0) throw new Error('尺寸必须大于0');
  352. clipObj = new fabric.Rect({
  353. width: width,
  354. height: height,
  355. left: offsetX,
  356. top: offsetY,
  357. rx: rectRadius,
  358. ry: rectRadius,
  359. absolutePositioned: true
  360. });
  361. } else if(shape === 'circle') {
  362. if (radius <= 0) throw new Error('半径必须大于0');
  363. clipObj = new fabric.Circle({
  364. radius: radius,
  365. left: offsetX,
  366. top: offsetY,
  367. absolutePositioned: true
  368. });
  369. }
  370. // 应用剪裁区域
  371. this.editLayer.set({ clipPath: clipObj });
  372. // 更新画布
  373. this.updateClipStroke()
  374. this.updateCanvasState();
  375. this.fcanvas.requestRenderAll();
  376. } catch (error) {
  377. console.error('剪裁区域创建失败:', error);
  378. this.$message.error('剪裁设置错误: ' + error.message);
  379. }
  380. },
  381. updateClipStroke() {
  382. if (!this.judge() || !this.clipSettings.showClipStroke) return;
  383. return;;
  384. const { shape, width, height, radius, offsetX, offsetY, strokeColor, strokeWidth, rectRadius } = this.clipSettings;
  385. // 移除当前对象已有的描边图形(如果存在)
  386. if (this.editLayer.strokeObj) {
  387. this.fcanvas.remove(this.editLayer.strokeObj);
  388. this.editLayer.strokeObj = null;
  389. }
  390. // 创建新的描边图形
  391. let strokeObj;
  392. if (shape === 'rect') {
  393. strokeObj = new fabric.Rect({
  394. width: width,
  395. height: height,
  396. left: offsetX,
  397. top: offsetY,
  398. rx: rectRadius,
  399. ry: rectRadius,
  400. stroke: strokeColor,
  401. sort:this.editLayer.sort,
  402. strokeWidth: strokeWidth,
  403. fill: null,
  404. selectable: false,
  405. evented: false
  406. });
  407. } else {
  408. strokeObj = new fabric.Circle({
  409. radius: radius,
  410. left: offsetX,
  411. top: offsetY,
  412. sort:this.editLayer.sort,
  413. stroke: strokeColor,
  414. strokeWidth: strokeWidth,
  415. fill: null,
  416. selectable: false,
  417. evented: false
  418. });
  419. }
  420. // 将描边图形绑定到当前对象
  421. this.editLayer.strokeObj = strokeObj;
  422. this.fcanvas.add(markRaw(strokeObj));
  423. this.fcanvas.requestRenderAll();
  424. },
  425. removeClipStroke() {
  426. if (this.clipStrokeObject) {
  427. this.fcanvas.remove(this.clipStrokeObject);
  428. this.clipStrokeObject = null;
  429. }
  430. },
  431. clearClipPath() {
  432. if (!this.judge()) return;
  433. // 清除剪裁路径
  434. this.editLayer.set({ clipPath: null });
  435. // 移除描边图形(如果存在)
  436. if (this.editLayer.strokeObj) {
  437. this.fcanvas.remove(this.editLayer.strokeObj);
  438. this.editLayer.strokeObj = null;
  439. }
  440. // 更新画布状态
  441. this.updateCanvasState();
  442. this.fcanvas.requestRenderAll();
  443. }
  444. }
  445. }